def k(self): """ Return a range in wavenumbers, set by the minimum/maximum allowed values, given the desired `kmin` and `kmax` and the current values of the AP effect parameters, `alpha_perp` and `alpha_par` """ return np.logspace(np.log10(self.kmin), np.log10(self.kmax), self.Nk)
def k(self): """ Return a range in wavenumbers, set by the minimum/maximum allowed values, given the desired `kmin` and `kmax` and the current values of the AP effect parameters, `alpha_perp` and `alpha_par` """ kmin = self.kmin if kmin < INTERP_KMIN: kmin = INTERP_KMIN kmax = self.kmax if kmax > INTERP_KMAX: kmax = INTERP_KMAX return np.logspace(np.log10(kmin), np.log10(kmax), self.Nk)
def __init__(self, window, ells, kmin=1e-4, kmax=0.7, Nk=1024, Nmu=40, max_ellprime=4): # make the grid # NOTE: we want to use the centers of the mu bins here! k = np.logspace(np.log10(kmin), np.log10(kmax), Nk) mu_edges = np.linspace(0., 1., Nmu+1) mu = 0.5 * (mu_edges[1:] + mu_edges[:-1]) grid_k, grid_mu = np.meshgrid(k, mu, indexing='ij') weights = np.ones_like(grid_k) grid = PkmuGrid([k,mu], grid_k, grid_mu, weights) # init the base class GriddedMultipoleTransfer.__init__(self, grid, ells, kmin=kmin, kmax=kmax) # the convolver object self.convolver = WindowConvolution(window[:,0], window[:,1:], max_ellprime=max_ellprime, max_ell=max(ells))
def stochasticity(self, k): """ The isotropic (type B) stochasticity term due to the discreteness of the halos, i.e., Poisson noise at 1st order. Notes ----- * The model for the (type B) stochasticity, interpolated as a function of sigma8(z), b1, and k using a Gaussian process """ _k = np.logspace(np.log10(self.k.min()), np.log10(self.k.max()), GP_NK) params = {'sigma8_z': self.sigma8_z, 'k': _k} if self._ib1 != self._ib1_bar: b1_1, b1_2 = sorted([self._ib1, self._ib1_bar]) toret = self.cross_stochasticity_fits(b1_1=b1_1, b1_2=b1_2, **params) else: toret = self.auto_stochasticity_fits(b1=self._ib1, **params) return spline(_k, toret)(k)
def __call__(self, power, k_out=None, extrap=False, mcfit_kwargs={}, **kws): """ Evaluate the convolved multipoles. Parameters ---------- power : xarray.DataArray a DataArray holding the :math:`P(k,\mu)` values on a coordinate grid with ``k`` and ``mu`` dimensions. k_out : array_like, optional if provided, evaluate the convolved multipoles at these ``k`` values using a spline **kws : additional keywords for testing purposes Returns ------- Pell : xarray.DataArray a DataArray holding the convolved :math:`P_\ell(k)` on a coordinate grid with ``k`` and ``ell`` dimensions. """ from pyRSD.extern import mcfit # get testing keywords dry_run = kws.get('dry_run', False) no_convolution = kws.get('no_convolution', False) # get the unconvovled theory multipoles Pell0 = GriddedMultipoleTransfer.__call__(self, power) # create additional logspaced k values for zero-padding up to k=100 h/Mpc oldk = Pell0['k'].values dk = np.diff(np.log10(oldk))[0] newk = 10**(np.arange(np.log10(oldk.max()) + dk, 2 + 0.5*dk, dk)) newk = np.concatenate([oldk, newk]) # now copy over with zeros Nk = len(newk); Nell = Pell0.shape[1] Pell = xr.DataArray(np.zeros((Nk,Nell)), coords={'k':newk, 'ell':Pell0.ell}, dims=['k', 'ell']) Pell.loc[dict(k=Pell0['k'])] = Pell0[:] # do the convolution if not no_convolution: # FFT the input power multipoles xi = np.empty((Nk, Nell), order='F') # column-continuous for i, ell in enumerate(self.ells): P2xi = mcfit.P2xi(newk, l=ell, **mcfit_kwargs) rr, xi[:,i] = P2xi(Pell.sel(ell=ell).values, extrap=extrap) # the linear combination of multipoles if dry_run: xi_conv = xi.copy() else: xi_conv = self.convolver(self.ells, rr, xi, order='F') # FFTLog back Pell_conv = np.empty((Nk, Nell), order='F') for i, ell in enumerate(self.ells): xi2P = mcfit.xi2P(rr, l=ell, **mcfit_kwargs) kk, Pell_conv[:,i] = xi2P(xi_conv[:,i], extrap=extrap) else: Pell_conv = Pell # interpolate to k_out coords = coords={'ell':Pell0.ell} if k_out is not None: shape = (len(k_out), len(self.ells)) toret = np.ones(shape) * np.nan for i, ell in enumerate(self.ells): idx = np.isfinite(newk) spl = spline(newk[idx], Pell_conv[idx,i]) toret[:,i] = spl(k_out) coords['k'] = k_out else: toret = Pell_conv coords['k'] = newk return xr.DataArray(toret, coords=coords, dims=['k', 'ell'])
class DarkMatterSpectrum(Cache, SimLoaderMixin, PTIntegralsMixin): """ The dark matter power spectrum in redshift space """ # splines and interpolation variables k_interp = np.logspace(np.log10(INTERP_KMIN), np.log10(INTERP_KMAX), 250) spline = tools.RSDSpline spline_kwargs = {'bounds_error': True, 'fill_value': 0} def __init__(self, kmin=1e-3, kmax=0.5, Nk=200, z=0., params=cosmology.Planck15, include_2loop=False, transfer_fit="CLASS", max_mu=4, interpolate=True, k0_low=5e-3, linear_power_file=None, Pdv_model_type='jennings', redshift_params=['f', 'sigma8_z'], **kwargs): """ Parameters ---------- kmin : float, optional The minimum wavenumber to compute the power spectrum at [units: `h/Mpc`] kmax : float, optional The maximum wavenumber to compute the power spectrum at [units: `h/Mpc`] Nk : int, optional The number of log-spaced bins to use as the underlying domain for splines z : float, optional The redshift to compute the power spectrum at. Default = 0. params : pyRSD.cosmology.Cosmology, str Either a Cosmology instance or the name of a file to load parameters from; see the 'data/params' directory for examples include_2loop : bool, optional If `True`, include 2-loop contributions in the model terms. Default is `False`. transfer_fit : str, optional The name of the transfer function fit to use. Default is `CLASS` and the options are {`CLASS`, `EH`, `EH_NoWiggle`, `BBKS`}, or the name of a data file holding (k, T(k)) max_mu : {0, 2, 4, 6, 8}, optional Only compute angular terms up to mu**(``max_mu``). Default is 4. interpolate: bool, optional Whether to return interpolated results for underlying power moments k0_low : float, optional (`5e-3`) below this wavenumber, evaluate any power in "low-k mode", which essentially just uses SPT at low-k linear_power_file : str, optional (`None`) string specifying the name of a file which gives the linear power spectrum, from which the transfer function in ``cosmo`` will be initialized Pdv_model_type : str, optional ('Jennings') the type of model to use for the density-velocity cross-correlation term, `Pdv`; either `Jennings` or `sims`. The Jennnings model is from arxiv: 1207.1439 redshift_params : list of str, optional the names of parameters to be updated when redshift changes """ # overload cosmo with a cosmo_filename kwargs to handle deprecated syntax if 'cosmo_filename' in kwargs: params = kwargs.pop('cosmo_filename') # set and save the model version automatically self.__version__ = __version__ # mix in the sim loader class SimLoaderMixin.__init__(self) # set the input parameters self.interpolate = interpolate self.transfer_fit = transfer_fit self.params = params self.max_mu = max_mu self.include_2loop = include_2loop self.kmin = kmin self.kmax = kmax self.Nk = Nk self.k0_low = k0_low self.linear_power_file = linear_power_file self.Pdv_model_type = Pdv_model_type # set these last self.redshift_params = redshift_params self.z = z # initialize the cosmology parameters and set defaults self.sigma8_z = self.cosmo.Sigma8_z(self.z) self.f = self.cosmo.f_z(self.z) self.alpha_par = 1. self.alpha_perp = 1. self.sigma_v2 = 0. self.sigma_bv2 = 0. self.sigma_bv4 = 0. # set the models we want to use # default is to use all models for kw in self.allowable_kwargs: if fnmatch.fnmatch(kw, 'use_*_model'): setattr(self, kw, kwargs.pop(kw, True)) # extra keywords if len(kwargs): for k in kwargs: warnings.warn("extra keyword `%s` is ignored" % k) # mix in the PT intergrals mixin PTIntegralsMixin.__init__(self) def __getstate__(self): """ Custom pickling that removes `lru_cache` objects from the cache, which will ensure pickling succeeds """ d = self.__dict__ for k in list(self._cache): if hasattr(self._cache[k], 'cache_info'): d['_cache'].pop(k) return d def initialize(self): """ Initialize the underlying splines, etc of the model """ k = 0.5 * (self.kmin + self.kmax) return self.power(k, 0.5) @contextlib.contextmanager def use_cache(self): """ Cache repeated calls to functions defined in this class, assuming constant `k` and `mu` input values """ from pyRSD.rsd.tools import cache_on, cache_off try: cache_on() yield except: raise finally: cache_off() #--------------------------------------------------------------------------- # parameters #--------------------------------------------------------------------------- @contextlib.contextmanager def preserve(self, **kwargs): """ Context manager that preserves the state of the model upon exiting the context by first saving and then restoring it """ # save the current state of the model set_params = {} unset_params = [] for k in self._param_names: if '__' + k in self.__dict__: set_params[k] = getattr(self, k) else: unset_params.append(k) cache = self._cache.copy() # current model params model_params = {} for k in self.allowable_kwargs: model_params[k] = getattr(self, k) # set any kwargs passed for k in kwargs: if k not in self.allowable_kwargs: raise ValueError( "keywords to this function must be in `allowable_kwargs`") setattr(self, k, kwargs[k]) yield # restore model params for k in model_params: setattr(self, k, model_params[k]) # restore the model to previous state for k in set_params: setattr(self, k, set_params[k]) for k in unset_params: if '__' + k in self.__dict__: delattr(self, k) for k in cache: self._cache[k] = cache[k] @contextlib.contextmanager def use_spt(self): """ Context manager to turn off all models """ from collections import OrderedDict params = OrderedDict() for kw in self.allowable_kwargs: if fnmatch.fnmatch(kw, 'use_*_model'): params[kw] = False try: # save the original state state = {k: getattr(self, k, None) for k in params} # update the state to low-k mode for k, v in params.items(): if hasattr(self, k): setattr(self, k, v) yield except: pass finally: # restore the original state for k, v in state.items(): if hasattr(self, k): setattr(self, k, v) @contextlib.contextmanager def load_dm_sims(self, val): """ Context manager to load simulation data for certain dark matter terms """ allowed = ['teppei_lowz', 'teppei_midz', 'teppei_highz'] if val not in allowed: raise ValueError("Allowed simulations to load are %s" % allowed) z_tags = { 'teppei_lowz': '000', 'teppei_midz': '509', 'teppei_highz': '989' } z_tag = z_tags[val] # get the data P00_mu0_data = getattr(sim_data, 'P00_mu0_z_0_%s' % z_tag)() P01_mu2_data = getattr(sim_data, 'P01_mu2_z_0_%s' % z_tag)() P11_mu4_data = getattr(sim_data, 'P11_mu4_z_0_%s' % z_tag)() Pdv_mu0_data = getattr(sim_data, 'Pdv_mu0_z_0_%s' % z_tag)() self._load('P00_mu0', P00_mu0_data[:, 0], P00_mu0_data[:, 1]) self._load('P01_mu2', P01_mu2_data[:, 0], P01_mu2_data[:, 1]) self._load('P11_mu4', P11_mu4_data[:, 0], P11_mu4_data[:, 1]) self._load('Pdv', Pdv_mu0_data[:, 0], Pdv_mu0_data[:, 1]) yield self._unload('P00_mu0') self._unload('P01_mu2') self._unload('P11_mu4') self._unload('Pdv') @parameter def interpolate(self, val): """ Whether we want to interpolate any underlying models """ self._update_models('interpolate', ['hzpt'], val) return val @parameter def transfer_fit(self, val): """ The transfer function fitting method """ allowed = ['CLASS', 'EH', 'EH_NoWiggle', 'BBKS'] if val not in allowed: raise ValueError("`transfer_fit` must be one of %s" % allowed) return val @parameter def cosmo_filename(self, val): """ The cosmology filename; this is deprecated, use attr:`params` instead """ self.params = val return val @parameter def params(self, val, default="cosmo_filename"): """ The cosmology parameters by the user """ # check cosmology object if val is None: val = cosmology.Planck15 if not isinstance( val, (string_types, cosmology.Cosmology, dict, pygcl.Cosmology)): raise TypeError( ("input `cosmo` keyword should be a parameter file name" "dictionary, or a pyRSD.cosmology.Cosmology object")) # convert from dictionary if isinstance(val, dict): val = cosmology.Cosmology(**val) # name of available cosmo? if isinstance(val, string_types) and hasattr(cosmology, val): val = getattr(cosmology, val) return val @parameter def linear_power_file(self, val): """ The name of the file holding the cosmological parameters """ return val @parameter def max_mu(self, val): """ Only compute the power terms up to and including `max_mu`. Should be one of [0, 2, 4, 6] """ allowed = [0, 2, 4, 6] if val not in allowed: raise ValueError("`max_mu` must be one of %s" % allowed) return val @parameter def include_2loop(self, val): """ Whether to include 2-loop terms in the power spectrum calculation """ return val @parameter def z(self, val): """ Redshift to evaluate power spectrum at """ # update the dependencies models = ['P11_sim_model', 'Pdv_sim_model'] self._update_models('z', models, val) # update redshift params if len(self.redshift_params): if 'f' in self.redshift_params: self.f = self.cosmo.f_z(val) if 'sigma8_z' in self.redshift_params: self.sigma8_z = self.cosmo.Sigma8_z(val) return val @parameter def k0_low(self, val): """ Wavenumber to transition to use only SPT for lower wavenumber """ if val < 5e-4: raise ValueError("`k0_low` must be greater than 5e-4 h/Mpc") return val @parameter def kmin(self, val): """ Minimum observed wavenumber needed for results """ if val < INTERP_KMIN: raise ValueError( "kmin = %.2e h/Mpc below PT integrals interpolation lower bound" % val) return val @parameter def kmax(self, val): """ Maximum observed wavenumber needed for results """ if val > INTERP_KMAX: raise ValueError( "kmax = %.2e h/Mpc above PT integrals interpolation upper bound" % val) return val @parameter def Nk(self, val): """ Number of log-spaced wavenumber bins to use in underlying splines """ return val @parameter def sigma8_z(self, val): """ The value of Sigma8(z) (mass variances within 8 Mpc/h at z) to compute the power spectrum at, which gives the normalization of the linear power spectrum """ # update the dependencies models = ['hzpt', 'P11_sim_model', 'Pdv_sim_model'] self._update_models('sigma8_z', models, val) return val @parameter def f(self, val): """ The growth rate, defined as the `dlnD/dlna`. """ # update the dependencies models = ['hzpt', 'Pdv_sim_model', 'P11_sim_model'] self._update_models('f', models, val) return val @parameter def alpha_perp(self, val): """ The perpendicular Alcock-Paczynski effect scaling parameter, where :math: `k_{perp, true} = k_{perp, true} / alpha_{perp}` """ return val @parameter def alpha_par(self, val): """ The parallel Alcock-Paczynski effect scaling parameter, where :math: `k_{par, true} = k_{par, true} / alpha_{par}` """ return val @parameter(default=1.0) def alpha_drag(self, val): """ The ratio of the sound horizon in the fiducial cosmology to the true cosmology """ return val @parameter(default="sigma_lin") def sigma_v(self, val): """ The velocity dispersion at `z`. If not provided, defaults to the linear theory prediction (as given by `self.sigma_lin`) [units: Mpc/h] """ return val @parameter def sigma_v2(self, val): """ The additional, small-scale velocity dispersion, evaluated using the halo model and weighted by the velocity squared. [units: km/s] .. math:: (sigma_{v2})^2 = (1/\bar{rho}) * \int dM M \frac{dn}{dM} v_{\parallel}^2 """ return val @parameter def sigma_bv2(self, val): """ The additional, small-scale velocity dispersion, evaluated using the halo model and weighted by the bias times velocity squared. [units: km/s] .. math:: (sigma_{bv2})^2 = (1/\bar{rho}) * \int dM M \frac{dn}{dM} b(M) v_{\parallel}^2 """ return val @parameter def sigma_bv4(self, val): """ The additional, small-scale velocity dispersion, evaluated using the halo model and weighted by bias times the velocity squared. [units: km/s] .. math:: (sigma_{bv4})^4 = (1/\bar{rho}) * \int dM M \frac{dn}{dM} b(M) v_{\parallel}^4 """ return val #--------------------------------------------------------------------------- # cached properties #--------------------------------------------------------------------------- @cached_property("cosmo") def fiducial_rs_drag(self): """ The sound horizon at the drag redshift in the cosmology of ``cosmo`` """ return self.cosmo.rs_drag() @cached_property("transfer_fit") def transfer_fit_int(self): """ The integer value representing the transfer function fitting method """ return getattr(pygcl.transfers, self.transfer_fit) @cached_property("params", "transfer_fit", "linear_power_file") def cosmo(self): """ A `pygcl.Cosmology` object holding the cosmological parameters """ # convert from cosmology.Cosmology to pygcl.Cosmology if isinstance(self.params, cosmology.Cosmology): kws = { 'transfer': self.transfer_fit_int, 'linear_power_file': self.linear_power_file } return self.params.to_class(**kws) # convert string to pygcl.Cosmology if self.linear_power_file is not None: k, Pk = np.loadtxt(self.linear_power_file, unpack=True) return pygcl.Cosmology.from_power(self.params, k, Pk) else: return pygcl.Cosmology(self.params, self.transfer_fit_int) @cached_property("Nk", "kmin", "kmax") def k(self): """ Return a range in wavenumbers, set by the minimum/maximum allowed values, given the desired `kmin` and `kmax` and the current values of the AP effect parameters, `alpha_perp` and `alpha_par` """ kmin = self.kmin if kmin < INTERP_KMIN: kmin = INTERP_KMIN kmax = self.kmax if kmax > INTERP_KMAX: kmax = INTERP_KMAX return np.logspace(np.log10(kmin), np.log10(kmax), self.Nk) @cached_property("z", "cosmo") def D(self): """ The growth function at z """ return self.cosmo.D_z(self.z) @cached_property("z", "cosmo") def conformalH(self): """ The conformal Hubble parameter, defined as `H(z) / (1 + z)` """ return self.cosmo.H_z(self.z) / (1. + self.z) @cached_property("cosmo") def power_lin(self): """ A 'pygcl.LinearPS' object holding the linear power spectrum at z = 0 """ return pygcl.LinearPS(self.cosmo, 0.) @cached_property("cosmo") def power_lin_nw(self): """ A 'pygcl.LinearPS' object holding the linear power spectrum at z = 0, using the Eisenstein-Hu no-wiggle transfer function """ cosmo = self.cosmo.clone(tf=pygcl.transfers.EH_NoWiggle) return pygcl.LinearPS(cosmo, 0.) @cached_property("sigma8_z", "cosmo") def _power_norm(self): """ The factor needed to normalize the linear power spectrum in `power_lin` to the desired sigma_8, as specified by `sigma8_z`, and the desired redshift `z` """ return (self.sigma8_z / self.cosmo.sigma8())**2 @cached_property("_power_norm", "_sigma_lin_unnormed") def sigma_lin(self): """ The dark matter velocity dispersion at z, as evaluated in linear theory [units: Mpc/h]. Normalized to `sigma8_z` """ return self._power_norm**0.5 * self._sigma_lin_unnormed @cached_property("power_lin") def _sigma_lin_unnormed(self): """ The dark matter velocity dispersion at z = 0, as evaluated in linear theory [units: Mpc/h]. This is not properly normalized """ return np.sqrt(self.power_lin.VelocityDispersion()) #--------------------------------------------------------------------------- # models to use #--------------------------------------------------------------------------- @parameter def use_P00_model(self, val): """ If `True`, use the `HZPT` model for P00 """ return val @parameter def use_P01_model(self, val): """ If `True`, use the `HZPT` model for P01 """ return val @parameter def use_P11_model(self, val): """ If `True`, use the `HZPT` model for P11 """ return val @parameter def use_Pdv_model(self, val): """ Whether to use interpolated sim results for Pdv """ return val @parameter def use_Pvv_model(self, val): """ Whether to use Jennings model for Pvv or SPT """ return val @parameter def Pdv_model_type(self, val): """ Either `jennings` or `sims` to describe the Pdv model """ allowed = ['jennings', 'sims'] if val not in allowed: raise ValueError("`Pdv_model_type` must be one of %s" % str(allowed)) return val @parameter def redshift_params(self, val): """ A list of parameter names to be scaled with redshift """ if len(val) and any(par not in ['f', 'sigma8_z'] for par in val): raise ValueError( "valid parameters for redshift scaling: 'f' and 'sigma8_z'") return val @cached_property("cosmo") def hzpt(self): """ The class holding the (possibly interpolated) HZPT models """ kw = {'interpolate': self.interpolate} return InterpolatedHZPTModels(self.cosmo, self.sigma8_z, self.f, **kw) @cached_property("power_lin") def P11_sim_model(self): """ The class holding the model for the P11 dark matter term, based on interpolating simulations """ return SimulationP11(self.power_lin, self.z, self.sigma8_z, self.f) @cached_property("power_lin") def Pdv_sim_model(self): """ The class holding the model for the Pdv dark matter term, based on interpolating simulations """ return SimulationPdv(self.power_lin, self.z, self.sigma8_z, self.f) def Pdv_jennings(self, k): """ Return the density-divergence cross spectrum using the fitting formula from Jennings et al 2012 (arxiv: 1207.1439) """ a0 = -12483.8 a1 = 2.554 a2 = 1381.29 a3 = 2.540 D = self.D s8 = self.sigma8_z / D # z = 0 results self.hzpt._P00.sigma8_z = s8 P00_z0 = self.hzpt.P00(k) # redshift scaling z_scaling = 3. / (D + D**2 + D**3) # reset sigma8_z self.hzpt._P00.sigma8_z = self.sigma8_z g = (a0 * P00_z0**0.5 + a1 * P00_z0**2) / (a2 + a3 * P00_z0) toret = (g - P00_z0) / z_scaling**2 + self.hzpt.P00(k) return -self.f * toret def Pvv_jennings(self, k): """ Return the divergence auto spectrum using the fitting formula from Jennings et al 2012 (arxiv: 1207.1439) """ a0 = -12480.5 a1 = 1.824 a2 = 2165.87 a3 = 1.796 D = self.D s8 = self.sigma8_z / D # z = 0 results self.hzpt._P00.sigma8_z = s8 P00_z0 = self.hzpt.P00(k) # redshift scaling z_scaling = 3. / (D + D**2 + D**3) # reset sigma8_z self.hzpt._P00.sigma8_z = self.sigma8_z g = (a0 * P00_z0**0.5 + a1 * P00_z0**2) / (a2 + a3 * P00_z0) toret = (g - P00_z0) / z_scaling**2 + self.hzpt.P00(k) return self.f**2 * toret def to_npy(self, filename): """ Save to a ``.npy`` file by calling :func:`numpy.save` """ np.save(filename, self) @classmethod def from_npy(cls, filename): """ Load a model from a ``.npy`` file """ from pyRSD.rsd import load_model return load_model(filename) #--------------------------------------------------------------------------- # utility functions #--------------------------------------------------------------------------- def update(self, **kwargs): """ Update the attributes. Checks that the current value is not equal to the new value before setting. """ for k, v in kwargs.items(): try: setattr(self, k, v) except Exception as e: raise RuntimeError( "failure to set parameter `%s` to value %s: %s" % (k, str(v), str(e))) @classmethod def default_config(cls, **params): """ Return a :class:`ParameterSet` object holding the default model configuration parameters, updated with any values passed on as keywords Parameters ---------- **params : parameter values specified as key/value pairs """ from pyRSD.rsdfit.parameters import Parameter, ParameterSet allowed = cls.allowable_kwargs model = cls() for name in allowed: params.setdefault(name, getattr(model, name)) # make the set of parameters toret = ParameterSet() for name in params: toret[name] = Parameter(name=name, value=params[name]) toret.tag = 'model' return toret @property def config(self): """ Return a dictionary holding the model configuration This holds the value of all attributes in the :attr:`allowable_kwargs` list """ allowed = self.__class__.allowable_kwargs return {k: getattr(self, k) for k in allowed} def _update_models(self, name, models, val): """ Update the specified attribute for the models given """ for model in models: if model in self._cache: setattr(getattr(self, model), name, val) #--------------------------------------------------------------------------- # power term attributes #--------------------------------------------------------------------------- def normed_power_lin(self, k): """ The linear power evaluated at the specified `k` and at `z`, normalized to `sigma8_z` """ return self._power_norm * self.power_lin(k) def normed_power_lin_nw(self, k): """ The Eisenstein-Hu no-wiggle, linear power evaluated at the specified `k` and at `self.z`, normalized to `sigma8_z` """ return self._power_norm * self.power_lin_nw(k) @interpolated_function("P00", "k", interp="k") def P_mu0(self, k): """ The full power spectrum term with no angular dependence. Contributions from P00. """ return self.P00.mu0(k) @interpolated_function("P01", "P11", "P02", "k", interp="k") def P_mu2(self, k): """ The full power spectrum term with mu^2 angular dependence. Contributions from P01, P11, and P02. """ return self.P01.mu2(k) + self.P11.mu2(k) + self.P02.mu2(k) @interpolated_function("P11", "P02", "P12", "P22", "P03", "P13", "P04", "include_2loop", "k", interp="k") def P_mu4(self, k): """ The full power spectrum term with mu^4 angular dependence. Contributions from P11, P02, P12, P22, P03, P13 (2-loop), and P04 (2-loop). """ Pk = self.P11.mu4(k) + self.P02.mu4(k) + self.P12.mu4( k) + self.P22.mu4(k) + self.P03.mu4(k) if self.include_2loop: Pk += self.P13.mu4(k) + self.P04.mu4(k) return Pk @interpolated_function("P12", "P22", "P13", "P04", "include_2loop", "k", interp="k") def P_mu6(self, k): """ The full power spectrum term with mu^6 angular dependence. Contributions from P12, P22, P13, and P04 (2-loop). """ Pk = self.P12.mu6(k) + self.P22.mu6(k) + self.P13.mu6(k) if self.include_2loop: Pk += self.P04.mu6(k) return Pk @interpolated_function("z", "_power_norm", "k", interp="k") def Pdd(self, k): """ The 1-loop auto-correlation of density. """ norm = self._power_norm return norm * (self.power_lin(k) + norm * self._Pdd_0(k)) @interpolated_function("f", "z", "_power_norm", "use_Pdv_model", "Pdv_model_type", "Pdv_loaded", "k", interp="k") def Pdv(self, k): """ The 1-loop cross-correlation between dark matter density and velocity divergence. """ # check for any user-loaded values if self.Pdv_loaded: return self._get_loaded_data('Pdv', k) else: if self.use_Pdv_model: if self.Pdv_model_type == 'jennings': return self.Pdv_jennings(k) elif self.Pdv_model_type == 'sims': return self.Pdv_sim_model(k) else: norm = self._power_norm return (-self.f) * norm * (self.power_lin(k) + norm * self._Pdv_0(k)) @interpolated_function("f", "z", "_power_norm", "use_Pvv_model", "k", interp="k") def Pvv(self, k): """ The 1-loop auto-correlation of velocity divergence. """ if self.use_Pvv_model: return self.Pvv_jennings(k) else: norm = self._power_norm return self.f**2 * norm * (self.power_lin(k) + norm * self._Pvv_0(k)) @cached_property("z", "sigma8_z", "use_P00_model", "power_lin", "P00_mu0_loaded") def P00(self): """ The isotropic, zero-order term in the power expansion, corresponding to the density field auto-correlation. No angular dependence. """ from .P00 import P00PowerTerm return P00PowerTerm(self) @cached_property("f", "z", "sigma8_z", "use_P01_model", "power_lin", "max_mu", "P01_mu2_loaded") def P01(self): """ The correlation of density and momentum density, which contributes mu^2 terms to the power expansion. """ from .P01 import P01PowerTerm return P01PowerTerm(self) @cached_property("f", "z", "sigma8_z", "use_P11_model", "power_lin", "max_mu", "include_2loop", "P11_mu2_loaded", "P11_mu4_loaded") def P11(self): """ The auto-correlation of momentum density, which has a scalar portion which contributes mu^4 terms and a vector term which contributes mu^2*(1-mu^2) terms to the power expansion. This is the last term to contain a linear contribution. """ from .P11 import P11PowerTerm return P11PowerTerm(self) @cached_property("f", "z", "sigma8_z", "power_lin", "max_mu", "include_2loop", "sigma_v", "sigma_bv2", "P00") def P02(self): """ The correlation of density and energy density, which contributes mu^2 and mu^4 terms to the power expansion. There are no linear contributions here. """ from .P02 import P02PowerTerm return P02PowerTerm(self) @cached_property("f", "z", "sigma8_z", "power_lin", "max_mu", "include_2loop", "sigma_v", "sigma_bv2", "P01") def P12(self): """ The correlation of momentum density and energy density, which contributes mu^4 and mu^6 terms to the power expansion. There are no linear contributions here. Two-loop contribution uses the mu^2 contribution from the P01 term. """ from .P12 import P12PowerTerm return P12PowerTerm(self) @cached_property("f", "z", "sigma8_z", "power_lin", "max_mu", "include_2loop", "sigma_v", "sigma_bv2", "P00", "P02") def P22(self): """ The autocorelation of energy density, which contributes mu^4, mu^6, mu^8 terms to the power expansion. There are no linear contributions here. """ from .P22 import P22PowerTerm return P22PowerTerm(self) @cached_property("f", "z", "sigma8_z", "power_lin", "max_mu", "include_2loop", "sigma_v", "sigma_v2", "P01") def P03(self): """ The cross-corelation of density with the rank three tensor field ((1+delta)v)^3, which contributes mu^4 terms. """ from .P03 import P03PowerTerm return P03PowerTerm(self) @cached_property("f", "z", "sigma8_z", "power_lin", "max_mu", "include_2loop", "sigma_v", "sigma_bv2", "sigma_v2", "P11") def P13(self): """ The cross-correlation of momentum density with the rank three tensor field ((1+delta)v)^3, which contributes mu^6 terms at 1-loop order and mu^4 terms at 2-loop order. """ from .P13 import P13PowerTerm return P13PowerTerm(self) @cached_property("f", "z", "sigma8_z", "power_lin", "max_mu", "include_2loop", "sigma_v", "sigma_bv4", "P02", "P00") def P04(self): """ The cross-correlation of density with the rank four tensor field ((1+delta)v)^4, which contributes mu^4 and mu^6 terms, at 2-loop order. """ from .P04 import P04PowerTerm return P04PowerTerm(self) #--------------------------------------------------------------------------- # main user callables #--------------------------------------------------------------------------- @tools.broadcast_kmu def power(self, k, mu, flatten=False): """ The redshift space power spectrum as a function of ``k`` and ``mu`` This includes terms up to ``mu**self.max_mu``. Parameters ---------- k : float or array_like The wavenumbers in `h/Mpc` to evaluate the model at mu : float, array_like The mu values to evaluate the power at. Returns ------- pkmu : float, array_like The power model P(k, mu). If `mu` is a scalar, return dimensions are `(len(self.k), )`. If `mu` has dimensions (N, ), the return dimensions are `(len(k), N)`, i.e., each column corresponds is the model evaluated at different `mu` values. If `flatten = True`, then the returned array is raveled, with dimensions of `(N*len(self.k), )` """ # the return array pkmu = self._power(k, mu) if flatten: pkmu = np.ravel(pkmu, order='F') return pkmu def poles(self, k, ells, Nmu=40): """ The multipole moments of the redshift-space power spectrum Parameters ---------- k : float, array_like The wavenumbers to evaluate the power spectrum at, in `h/Mpc` ells : int, array_like The `ell` values of the multipole moments Nmu : int, optional the number of ``mu`` bins to use when performing the multipole integration Returns ------- poles : array_like returns tuples of arrays for each ell value in ``poles`` """ from pyRSD.rsd.transfers import MultipoleTransfer # the transfer t = MultipoleTransfer(k, ells, Nmu=Nmu) # evaluate the model on the grid P = self.power(t.flatk, t.flatmu) # return the transferred power return t(P) def apply_transfer(self, transfer): """ Return the power spectrum with the specified transfer functions applied, e.g., grid binning, multipole integration, etc. Parameters ---------- transfer : subclass of :class:`pyRSD.rsd.transfers.TransferBase` the transfer class """ # check some bounds for window convolution from pyRSD.rsd.transfers import WindowFunctionTransfer if isinstance(transfer, WindowFunctionTransfer): kmin = transfer.kmin kmax = transfer.kmax if (kmin < self.kmin): warnings.warn( "min k of window transfer (%s) is less than model's kmin (%.2e)" % (kmin, self.kmin)) if (kmax > self.kmax): warnings.warn( "max k of window transfer (%s) is greater than model's kmax (%.2e)" % (kmax, self.kmax)) # check bad values if self.kmin > 5e-3: warnings.warn( "doing window convolution with dangerous model kmin (%.2e)" % self.kmin) if self.kmax < 0.5: warnings.warn( "doing window convolution with dangerous model kmax (%.2e)" % self.kmax) # evaluate the model on the grid P = self.power(transfer.flatk, transfer.flatmu) # return power with transfer applied return transfer(P) @tools.alcock_paczynski def _P_mu0(self, k, mu): """ Return the AP-distorted P[mu^0] """ return self.P_mu0(k) @tools.alcock_paczynski def _P_mu2(self, k, mu): """ Return the AP-distorted mu^2 P[mu^2] """ return mu**2 * self.P_mu2(k) @tools.alcock_paczynski def _P_mu4(self, k, mu): """ Return the AP-distorted mu^4 P[mu^4] """ return mu**4 * self.P_mu4(k) @tools.alcock_paczynski def _P_mu6(self, k, mu): """ Return the AP-distorted mu^6 P[mu^6] """ return mu**6 * self.P_mu6(k) @tools.broadcast_kmu @tools.alcock_paczynski def derivative_k(self, k, mu): """ Return the derivative of :func:`power` with respect to `k` """ toret = 0 funcs = [self.P_mu0, self.P_mu2, self.P_mu4, self.P_mu6] i = 0 while i <= (self.max_mu // 2): toret += mu**(2 * i) * funcs[i](k, derivative=True) i += 1 return toret @tools.broadcast_kmu @tools.alcock_paczynski def derivative_mu(self, k, mu): """ Return the derivative of :func:`power` with respect to `mu` """ toret = 0 funcs = [self.P_mu0, self.P_mu2, self.P_mu4, self.P_mu6] i = 0 while i <= (self.max_mu // 2): # derivative of mu^(2i) if i != 0: toret += (2. * i) * mu**(2 * i - 1) * funcs[i](k) i += 1 return toret def _power(self, k, mu): """ Return the power as sum of mu powers """ if self.max_mu > 6: raise NotImplementedError( "cannot compute power spectrum including terms with order higher than mu^6" ) toret = 0 funcs = [self._P_mu0, self._P_mu2, self._P_mu4, self._P_mu6] i = 0 while i <= (self.max_mu // 2): toret += funcs[i](k, mu) i += 1 return np.nan_to_num(toret) @tools.monopole def monopole(self, k, mu, **kwargs): """ The monopole moment of the power spectrum. Include mu terms up to mu**max_mu. """ return self.power(k, mu, **kwargs) @tools.quadrupole def quadrupole(self, k, mu, **kwargs): """ The quadrupole moment of the power spectrum. Include mu terms up to mu**max_mu. """ return self.power(k, mu, **kwargs) @tools.hexadecapole def hexadecapole(self, k, mu, **kwargs): """ The hexadecapole moment of the power spectrum. Include mu terms up to mu**max_mu. """ return self.power(k, mu, **kwargs) @tools.tetrahexadecapole def tetrahexadecapole(self, k, mu, **kwargs): """ The tetrahexadecapole (ell=6) moment of the power spectrum """ return self.power(k, mu, **kwargs)
class QuasarSpectrum(HaloSpectrum): """ The quasar redshift space power spectrum, a subclass of :class:`~pyRSD.rsd.HaloSpectrum` for biased redshift space power spectra """ k_interp = np.logspace(np.log10(1e-8), np.log10(100.0), 500) def __init__(self, fog_model='gaussian', **kwargs): """ Initialize the QuasarSpectrum Parameters ---------- fog_model : str, optional the string specifying the FOG model to use; one of ['modified_lorentzian', 'lorentzian', 'gaussian']. Default is 'gaussian' """ # the base class super(QuasarSpectrum, self).__init__(**kwargs) # set the defaults self.fog_model = fog_model self.sigma_fog = 4.0 self.include_2loop = False self.N = 0 # fnl parameters self.f_nl = 0 self.p = 1.6 # good for quasars @cached_property("Nk", "kmin", "kmax") def k(self): """ Return a range in wavenumbers, set by the minimum/maximum allowed values, given the desired `kmin` and `kmax` and the current values of the AP effect parameters, `alpha_perp` and `alpha_par` """ return np.logspace(np.log10(self.kmin), np.log10(self.kmax), self.Nk) @parameter def kmin(self, val): """ Minimum observed wavenumber needed for results """ return val @parameter def kmax(self, val): """ Maximum observed wavenumber needed for results """ return val def default_params(self): """ Return a QuasarPowerParameters instance holding the default model parameters configuration The model associated with the parameter is ``self`` """ from pyRSD.rsdfit.theory import QuasarPowerParameters return QuasarPowerParameters.from_defaults(model=self) @parameter def p(self, val): """ The bias type for PNG; either 1.6 or 1 usually """ return val @parameter def fog_model(self, val): """ Function to return the FOG suppression factor, which reads in a single variable `x = k \mu \sigma` """ allowable = ['modified_lorentzian', 'lorentzian', 'gaussian'] if val not in allowable: raise ValueError("`fog_model` must be one of %s" % allowable) return val @parameter def sigma_fog(self, val): """ The FOG velocity dispersion in Mpc/h """ return val @parameter def p(self, val): """ The value encoding the type of tracer; must be between 1 and 1.6, with p=1 suitable for LRGs and p=1.6 suitable for quasars The scale-dependent bias is proportional to: ``b1 - p`` """ if not 1 <= val <= 1.6: raise ValueError( "f_nl ``p`` value should be between 1 and 1.6 (inclusive)") return val @parameter def f_nl(self, val): """ The amplitude of the local-type non-Gaussianity """ return val @parameter def N(self, val): """ Constant offset to model, set to 0 by default """ return val @cached_property("fog_model") def FOG(self): """ Return the FOG function """ return FOGKernel.factory(self.fog_model) #--------------------------------------------------------------------------- # primordial non-Gaussianity functions #--------------------------------------------------------------------------- @cached_property() def delta_crit(self): """ The usual critical overdensity value for collapse from Press-Schechter """ return 1.686 @interpolated_function("k", "cosmo", "D") def alpha_png(self, k): """ The primordial non-Gaussianity alpha value see e.g., equation 2 of Mueller et al 2017 """ # transfer function, normalized to unity Tk = (self.normed_power_lin(k)/k**self.cosmo.n_s())**0.5 Tk /= Tk.max() # normalization c = pygcl.Constants.c_light / pygcl.Constants.km # in km/s H0 = 100 # in units of h km/s/Mpc # normalizes growth function to unity in matter-dominated epoch g_ratio = growth_function(self.cosmo, 0.) g_ratio /= (1+100.) * growth_function(self.cosmo, 100.) return 2*k**2 * Tk * self.D / (3*self.cosmo.Omega0_m()) * (c/H0)**2 * g_ratio @interpolated_function("k", "b1", "f_nl", "p") def delta_bias(self, k): """ The scale-dependent bias introduced by primordial non-Gaussianity """ if self.f_nl == 0: return k*0. return 2*(self.b1-self.p)*self.f_nl*self.delta_crit/self.alpha_png(k) @interpolated_function("k", "b1", "delta_bias") def btot(self, k): """ The total bias, accounting for scale-dependent bias introduced by primordial non-Gaussianity """ return self.b1 + self.delta_bias(k) #--------------------------------------------------------------------------- # power as a function of mu #--------------------------------------------------------------------------- @interpolated_function("k", "sigma8_z", "_power_norm", "btot") def P_mu0(self, k): """ The isotropic part of the Kaiser formula """ return self.btot(k)**2 * self.normed_power_lin(k) @interpolated_function("k", "sigma8_z", "f", "_power_norm", "btot") def P_mu2(self, k): """ The mu^2 term of the Kaiser formula """ return 2*self.f*self.btot(k) * self.normed_power_lin(k) @interpolated_function("k", "sigma8_z", "f", "_power_norm") def P_mu4(self, k): """ The mu^4 term of the Kaiser formula """ return self.f**2 * self.normed_power_lin(k) @interpolated_function(interp="k") def P_mu6(self, k): """ The mu^6 term is zero in the Kaiser formula """ return k*0. @tools.broadcast_kmu @tools.alcock_paczynski def power(self, k, mu, flatten=False): """ Return the redshift space power spectrum at the specified value of mu, including terms up to ``mu**self.max_mu``. Parameters ---------- k : float or array_like The wavenumbers in `h/Mpc` to evaluate the model at mu : float, array_like The mu values to evaluate the power at. Returns ------- pkmu : float, array_like The power model P(k, mu). If `mu` is a scalar, return dimensions are `(len(self.k), )`. If `mu` has dimensions (N, ), the return dimensions are `(len(k), N)`, i.e., each column corresponds is the model evaluated at different `mu` values. If `flatten = True`, then the returned array is raveled, with dimensions of `(N*len(self.k), )` """ # the linear kaiser P(k,mu) pkmu = super(QuasarSpectrum, self).power(k, mu) # add FOG damping G = self.FOG(k, mu, self.sigma_fog) pkmu *= G**2 # add shot noise offset pkmu += self.N if flatten: pkmu = np.ravel(pkmu, order='F') return pkmu @tools.broadcast_kmu @tools.alcock_paczynski def derivative_k(self, k, mu): """ The derivative with respect to `k_AP` """ G = self.FOG(k, mu, self.sigma_fog) Gprime = self.FOG.derivative_k(k, mu, self.sigma_fog) deriv = super(QuasarSpectrum, self).derivative_k(k, mu) power = super(QuasarSpectrum, self).power(k, mu) return G**2 * deriv + 2 * G*Gprime * power @tools.broadcast_kmu @tools.alcock_paczynski def derivative_mu(self, k, mu): """ The derivative with respect to `mu_AP` """ G = self.FOG(k, mu, self.sigma_fog) Gprime = self.FOG.derivative_mu(k, mu, self.sigma_fog) deriv = super(QuasarSpectrum, self).derivative_mu(k, mu) power = super(QuasarSpectrum, self).power(k, mu) return G**2 * deriv + 2 * G*Gprime * power def get_gradient(self, pars): """ Return a :class:`PkmuGradient` object which can compute the gradient of :func:`GalaxySpectrum.power` for a set of desired parameters Parameters ---------- pars : ParameterSet """ from pyRSD.rsd.power.qso.derivatives import PqsoDerivative from pyRSD.rsd.power.gradient import PkmuGradient registry = PqsoDerivative.registry() return PkmuGradient(self, registry, pars)