geff.tools.pt
$\newcommand{\bm}[1]{\boldsymbol{#1}}$
This module is designed to compute the tensor power spectrum from a GEF result.
The polarized tensor power spectrum at time $t$ with momentum $k$ is given by $$\mathcal{P}_{T}(t, k) = \mathcal{P}_{T}^{\mathrm{vac}}(t, k) + \frac{1}{2}\sum_{\lambda=\pm 1} \mathcal{P}_{T,\lambda}^{\mathrm{ind}}(t, k)\, ,$$ where $\lambda$ labels the helicity of tensor modes.
The vacuum contribution is given by
$$\mathcal{P}_{T}^{\mathrm{vac}}(t, k) = \frac{4 k^3}{\pi^2 M_{\rm P}^2} |u_0(t, k)|^2\, ,$$
while the induced contribution is
$$\mathcal{P}_{T,\lambda}^{\mathrm{ind}}(t, k) = \frac{k^3}{2 \pi^2 M_{\rm P}^4} \int \frac{{\rm d}^3 \bm{p}}{(2 \pi)^3} \sum_{\alpha,\beta = \pm1} \left(1 + \lambda \alpha \frac{\bm{k} \cdot \bm{p}}{k p} \right)^2 \left(1 + \lambda \beta \frac{k^2 - \bm{k} \cdot \bm{p}}{kq} \right)^2 $$ $$ \qquad \qquad \qquad \times \left|\int_{-\infty}^\infty {\rm d} s \frac{G_k(t, s)}{a^3(s)} \left[A'_\alpha(s, p)A'_\beta(s, q) + \alpha \beta\, p q\, A_\alpha(s, p) A_\alpha(s, q) \right] \right|^2 \, , $$
with momentum $q = |\bm{p} + \bm{q}|$ and scale-factor $a$.
The vacuum modes $u_0(t,k)$ obey the mode equation $$\mathcal{D}_k {u_0} = \ddot{u}_0 + 3 H \dot{u}_0 + \frac{k^2}{a^2} {u_0} = 0$$ with the retarded Green function $G_k(t',t)$ defined for the operator $\mathcal{D}_k$.
The gauge-field mode functions $A_\lambda(t,k)$ are defined as in the geff.mode_by_mode module.
For details on the numerical computation, see the Appendix B of 2508.00798.
1import numpy as np 2from scipy.interpolate import CubicSpline, PchipInterpolator 3from scipy.integrate import solve_ivp, trapezoid 4 5from geff._docs.docs_pt import DOCS 6from geff.mbm import GaugeSpec 7from geff.bgtypes import BGSystem, Constant 8 9from typing import Tuple 10from types import NoneType 11 12__doc__ = DOCS["module"] 13 14class PowSpecT: 15 r""" 16 A class used to compute the tensor power spectrum including vacuum and gauge-field induced contributions. 17 18 These main method of this module is `compute_pt`, which computes both the vacuum and gauge-field induced contribution 19 to the tensor power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}$ and $\mathcal{P}_{T,\lambda}^{\mathrm{ind}}$. 20 21 Results are internally computed using numerical units, but are returned in physical units. 22 """ 23 def __init__(self, insys : BGSystem): 24 """ 25 Initialise the class from a GEF solution. 26 27 Parameters 28 ---------- 29 insys : BGSystem 30 the GEF solution. 31 """ 32 #Set GEF results to Hubble units. 33 sys = BGSystem.from_system(insys, True) 34 sys.units = False 35 36 #import the background evolution 37 N = sys.N.value 38 a = np.exp(N) 39 H = sys.H.value 40 41 self._omega = sys.omega 42 43 #Set the range of modes 44 self.maxk = max(a*H) 45 self.mink = 1e4 46 47 #Define Useful quantities 48 49 self._t = sys.t.value 50 self._N = N 51 self._H = H 52 self._xi= sys.xi.value 53 54 #define interpolated quantities as needed 55 self._af = CubicSpline(self._t, a) 56 if isinstance(self._H, Constant): 57 self._Hf = lambda t: self._H 58 else: 59 self._Hf = CubicSpline(self._t, H) 60 self._HN = CubicSpline(self._N, self._H) 61 self._khN = CubicSpline(N, sys.kh) 62 63 #Obtain eta as a functio of time for phases 64 def deta(t, y): return 1/self._af(t) 65 66 soleta = solve_ivp(deta, [min(self._t), max(self._t)], np.array([0]), t_eval=self._t) 67 68 self._etaf = CubicSpline(self._t, soleta.y[0,:]) 69 return 70 71 def compute_pt(self, nmodes : int, spec : GaugeSpec, FastGW : bool=True, 72 atols : list=[1e-3,1e-20], rtols : list=[1e-4,1e-4], momgrid : int=100 73 ) -> Tuple[np.ndarray,dict]: 74 r""" 75 Compute the full tensor power spectrum. 76 77 The method lodes data on gauge modes $A_\lambda(t,k)$ from a file indicated by `mbm_file`. 78 The power spectrum is evaluated fpr `nmodes` log-spaced momenta $k \in [10^{4}a(0)H(0), 10 a(t_{\rm max}) H(t_{\rm max})]$ 79 80 Parameters 81 ---------- 82 nmodes : NDArray 83 the number of momenta 84 ModePath : str 85 path to a file containing gauge-field mode functions 86 FastGW : bool 87 If `True`, only the expected dominant contributions to $\mathcal{P}_{T,\lambda}^{\mathrm{ind}}$ are computed 88 atols : list 89 absolute tolerance for `compute_homogeneous` (index 0) and `compute_green` (index 1) 90 rtols : list 91 relative tolerance for `compute_homogeneous` (index 0) and `compute_green` (index 1) 92 momgrid : int 93 passed to `compute_ptind` 94 95 Returns 96 ------- 97 ks : NDArray 98 the momenta $k$ 99 PT : dict 100 a dictionary containing all contributions to the tensor power spectrum and its total. 101 """ 102 103 k = np.logspace(np.log10(self.mink), np.log10(10*self.maxk), nmodes) 104 ks, tstarts = self._find_tinit_bd(k, mode="k") 105 106 Ngrid = spec["N"] 107 tgrid = spec["t"] 108 pgrid = spec["k"] 109 110 GaugeModes = {"+":(spec["Ap"], spec["dAp"]), "-":(spec["Am"], spec["dAm"])} 111 112 indend = np.argmin(abs(Ngrid-max(self._N))) 113 114 PT = {"tot":[], "vac":[], "ind+,++":[], "ind+,+-":[], "ind+,--":[], "ind-,++":[], "ind-,+-":[], "ind-,--":[]} 115 116 GWpols = [("+", 1), ("-",-1)] 117 118 gaugepols=[(("+",1),("+",1)), 119 (("+",1),("-",-1)), 120 (("-",-1),("-",-1))] 121 122 sign = np.sign(self._xi[0]) 123 124 for i, k in enumerate(ks): 125 tstart = tstarts[i] 126 127 if k > 10**(5/2)*self.maxk: 128 for key in PT.keys(): 129 PT[key].append(0) 130 else: 131 uk, _ = self.compute_homogeneous(k, tstart, tgrid, atol=atols[0], rtol=rtols[0]) 132 Green = self.compute_green(k, uk, tstart, tgrid, indend, atol=atols[1], rtol=rtols[1]) 133 134 PT["vac"].append(self.compute_ptvac(k, uk[indend])) 135 136 for lgrav in GWpols: 137 for mu in gaugepols: 138 GWpol = lgrav[1] 139 140 Ax = GaugeModes[mu[0][0]][0] 141 dAx = GaugeModes[mu[0][0]][1] 142 lx = mu[0][1] 143 Ay = GaugeModes[mu[1][0]][0] 144 dAy = GaugeModes[mu[1][0]][1] 145 ly = mu[1][1] 146 147 if FastGW and (lx != sign or ly != sign): 148 PT[f"ind{lgrav[0]},{mu[0][0]}{mu[1][0]}"].append(0.) 149 else: 150 PT[f"ind{lgrav[0]},{mu[0][0]}{mu[1][0]}"].append( 151 self.compute_ptind(k, GWpol, Ngrid, indend, Green, pgrid, lx, Ax, dAx, ly, Ay, dAy, momgrid) ) 152 153 PT["tot"] = np.zeros(ks.shape) 154 for key in PT.keys(): 155 PT[key] = np.array(PT[key]) 156 if ("+" in key) or ("-" in key): 157 PT["tot"] += 0.5*PT[key] 158 elif key=="vac": 159 PT["tot"] += PT[key] 160 161 #this returns k with units restored. Needs to be matched by omega_GW 162 return ks*self._omega, PT 163 164 def compute_homogeneous(self, k : float, tvac : float, teval : np.ndarray|NoneType, atol : float=1e-3, rtol : float=1e-4) -> Tuple[np.ndarray, np.ndarray]: 165 r""" 166 Compute the evolution of a vacuum mode starting from Bunch–Davies vacuum. 167 168 For a mode $u_0(t,k)$ with momentum $k$, initialise in Bunch–Davies when $k = 10^{5/2} a(t_{\rm vac})H(t_{\rm vac})$. 169 Its evolution is obtained by solving `tensor_mode_eq` using `scipy.integrate.solve_ivp`. 170 171 Parameters 172 ---------- 173 k : float 174 the momentum $k$. 175 tvac : float 176 the initial time, $t_{\rm init}$ 177 teval : NDArray or None 178 time coordinates $t$ of $u_0(t,k)$ (`None`: same as class input `sys`) 179 atol : float 180 the absolute tolerance for `solve_ivp` 181 rtol : float 182 the relative tolerance for `solve_ivp` 183 184 Returns 185 ------- 186 u : NDArray 187 the vacuum tensor modes, $\sqrt{2k}u(t, k)$ 188 duk : NDArray 189 the derivative of the vacuum tensor modes, $a\sqrt{2/k}\dot{u}(t, k)$ 190 """ 191 192 if len(teval)==0: 193 teval = self._t 194 tend = max(teval) 195 196 #conformal time needed for relative phases 197 eta = self._etaf(teval) 198 199 istart = 0 200 while teval[istart]<tvac: 201 istart+=1 202 203 #define the ODE for the GW modes 204 def ode(t, y): return self.tensor_mode_eq( y, k, self._Hf(t), self._af(t) ) 205 206 #Initialise the modes in Bunch Davies 207 Zini = np.array([1, -10**(-5/2), 0, -1]) 208 209 sol = solve_ivp(ode, [tvac, tend], Zini, t_eval=teval[istart:], method="RK45", atol=atol, rtol=rtol) 210 if not(sol.success): 211 print("Something went wrong") 212 213 #the mode was in vacuum before tvac 214 vac = list( np.exp(-1j*eta[:istart]*k) ) 215 dvac = list( -1j*np.exp(-1j*eta[:istart]*k) ) 216 217 #Create an array tracking a modes evolution from Bunch Davies to late times. Ensure equal length arrays for every mode k 218 uk = np.array( vac + list( (sol.y[0,:] + 1j*sol.y[2,:])*np.exp(-1j*k*eta[istart]) ) )/self._af(teval) 219 duk = np.array( dvac + list( (sol.y[1,:] + 1j*sol.y[3,:])*np.exp(-1j*k*eta[istart]) ) )/self._af(teval) 220 221 return uk, duk 222 223 @staticmethod 224 def tensor_mode_eq(y : np.ndarray, k : float, H : float, a : float) -> np.ndarray: 225 r""" 226 Mode equation for vacuum tensor modes. 227 228 Parameters 229 ---------- 230 y : NDArray 231 the vacuum tensor mode and its derivatives. 232 $a \sqrt{2k} u_0(t,k)$ and $ a^2\sqrt{2/k} \dot{u}_0(t, k)$ 233 k : float 234 comoving momentum $k$ 235 H : float 236 Hubble rate, $H(t)$ 237 a : float 238 scale factor, $a(t)$ 239 240 Returns 241 ------- 242 dydt : NDArray 243 an array of time derivatives of y 244 245 """ 246 dydt = np.zeros(y.shape) 247 248 #real 249 dydt[0] = (H*y[0] + k/a*y[1]) 250 dydt[1] = ( -H*y[1] - k/a*y[0] ) 251 252 #imaginary 253 dydt[2] = (H*y[2] + k/a*y[3]) 254 dydt[3] = ( -H*y[3] - k/a*y[2] ) 255 256 return dydt 257 258 def compute_green(self, k : float, uk : np.ndarray, tvac : float, teval : np.ndarray|NoneType, ind : int, atol : float=1e-30, rtol : float=1e-4) -> np.ndarray: 259 r""" 260 Compute the evolution of the Green function $G_k(t',t)$ for fixed $t'$. 261 262 The evolution is obtained by solving `green_ode` from $t=t'$ backwards until $t_{\rm vac}$, defined through $k = 10^{5/2} a(t_{\rm end})H(t_{\rm vac})$. 263 From $t_{\rm end}$ onwards, the Green function is instead computed from the mode $u_0(t, k)$. The method uses `scipy.integrate.solve_ivp`. 264 265 Parameters 266 ---------- 267 k : float 268 the momentum $k$. 269 uk : NDArray 270 the mode function $u_0(t,k)$ 271 tvac : float 272 the final time, $t_{\rm vac}$ 273 teval : NDArray or None 274 time coordinates $t$ of $G_k(t',t)$ (`None`: same as class input `sys`) 275 ind : int 276 index of teval corresponding to $t'$ 277 atol : float 278 the absolute tolerance for `solve_ivp` 279 rtol : float 280 the relative tolerance for `solve_ivp` 281 282 Return 283 ------ 284 GreenN : NDArray 285 the Green function $-k G_k(t', t)$ 286 """ 287 if len(teval)==0: 288 teval = self._t 289 290 istart = 0 291 while teval[istart]<tvac: 292 istart+=1 293 294 # G(k, t, t) = 0 by definition 295 Aini = np.array([0, 1]) 296 297 298 # Solve the EoM for G backwards in time starting from G(k, t, t) 299 def Aode(t, y): return -self.green_ode(y, k, self._Hf(-t), self._af(-t)) 300 solA = solve_ivp(Aode, [-teval[ind], -tvac], Aini, t_eval=-teval[istart:ind+1][::-1], 301 method="RK45", atol=atol, rtol=rtol) 302 303 #For numerical stability, only solve the EoM for the Green function until t' = tvac. Afterwards, compute it directly from the vacuum modes. 304 GreenN = np.zeros(teval.shape) 305 GreenN[istart:ind+1] = solA.y[0,:][::-1] 306 GreenN[:istart] = ( (uk[ind].conjugate()*uk).imag*self._af(teval)**2 )[:istart] 307 308 return GreenN 309 310 @staticmethod 311 def green_ode(y : np.ndarray, k : float, H : float, a : float) -> np.ndarray: 312 r""" 313 Evolve the re-scaled Green function for tensor modes in time. 314 315 The re-scaled Green function is 316 $$B_{t'}^k(t) = 2 k a(t)^2 \operatorname{Im} \left[ u_0^*(t', k) \, u_0(t,k) \right] = k G_k(t', t) \,.$$ 317 318 whose ODE is coupled to 319 320 $$C_{t'}^k(t) = 2 a(t)^3 \operatorname{Im} \left[ u_0^*(t', k) \, {\dot{u}_0}(t,k) \right] \,.$$ 321 322 Parameters 323 ---------- 324 y : NDArray 325 the functions B and C 326 k : float 327 comoving momentum $k$ 328 H : float 329 Hubble rate, $H(t)$ 330 a : float 331 scale factor, $a(t)$ 332 333 Returns 334 ------- 335 dydt : NDArray 336 an array of time derivatives of A 337 338 """ 339 dydt = np.zeros(y.shape) 340 dydt[0] =(2*H*y[0] + k/a*y[1]) 341 dydt[1] = -k/a*y[0] 342 return dydt 343 344 345 def _find_tinit_bd(self, init : np.ndarray, mode : str="k") -> Tuple[np.ndarray, np.ndarray]: 346 """ 347 Determines the pair of $k$ and $t$ satisfying $k = 10^(5/2)a(t)H(t)$. 348 349 Depending on `mode`, `init` may be a time coordinate (`mode='t'`), $e$-folds (`mode='N'`) or momentum (`mode='k'`). 350 351 Parameters 352 ---------- 353 init : array 354 the input array (t, N, or k) 355 mode : str 356 indicate the type of `init` 357 358 Returns 359 ------- 360 ks : NDarray 361 an array of momenta 362 tstart : NDarray 363 an array of times 364 365 Raises 366 ------ 367 KeyError 368 if `mode` is not 't, 'k' or 'N' 369 """ 370 371 pwr = 5/2 372 373 t = self._t 374 def logkH(t): return np.log(self._af(t)*self._Hf(t)) 375 if mode=="t": 376 tstart = init 377 logks = logkH(tstart) 378 ks = 10**(pwr)*np.exp(logks) 379 380 elif mode=="k": 381 ks = init 382 383 tstart = [] 384 for k in ks: 385 ttmp = self._t[np.searchsorted(10**(pwr)*np.exp(logkH(self._t)), k, "right")] 386 tstart.append(ttmp) 387 tstart = np.array(tstart) 388 389 elif mode=="N": 390 tstart = CubicSpline(self._N, t)(init) 391 ks = 10**pwr*np.exp(logkH(tstart)) 392 393 else: 394 raise KeyError("'mode' must be 't', 'k' or 'N'") 395 396 return ks, tstart 397 398 def compute_ptvac(self, k : np.ndarray, uk : np.ndarray) -> np.ndarray: 399 r""" 400 Compute the vacuum power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}(t,k)$. 401 402 Parameters 403 ---------- 404 k : NDArray 405 momentum, $k$ 406 uk : NDArray 407 vacuum tensor mode $u_0(t,k)$ 408 409 Return 410 ------ 411 PTvac : NDArray 412 the vacuum tensor power spectrum 413 """ 414 PTvac = 2*(k*self._omega)**2/( np.pi**2 ) * abs(uk)**2 415 return PTvac 416 417 def compute_ptind(self, k : float, lgrav : float, Ngrid : np.ndarray, ind: int, GreenN : np.ndarray, pgrid : np.ndarray, 418 l1 : float, A1 : np.ndarray, dA1 : np.ndarray, l2 : float, A2 : np.ndarray, dA2 : np.ndarray, 419 momgrid : int=100) -> float: 420 r""" 421 Compute the vacuum power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}(t,k)$. 422 423 The integral is computed by integrating over the inner integral over the gauge-mode functions $A_\mu(s,p)$ using `scipy.integrate.trapezoid`. 424 The external momentum integrals are also computed using `scipy.integrate.trapezoid` on a grid of momenta momgrid x momgrid. The mode functions 425 are interpolated over momentum to match this grid. 426 427 Parameters 428 ---------- 429 k : float 430 momentum $k$ 431 lgrav : NDArray 432 helicity $\lambda$ 433 Ngrid : NDArray 434 $e$-folds $\log a$ 435 ind : integer 436 the power spectrum is computed at $t$ corresponding to `Ngrid[ind]` 437 GreenN : NDArray 438 the Green function $kG_k(t, s)$ with $s$ corresponding to `Ngrid` 439 pgrid : NDArray 440 momenta $p$ for the mode functions $A_\mu(s,p)$ 441 l1, l2 : float 442 the gauge-field helicities $\mu_1$, $\mu_2$ 443 A1, A2 : float 444 the gauge-field mode functions $\sqrt{2k} A_{\mu_1, \mu_2}(s, p)$ 445 dA1, dA2 : float 446 the gauge-field mode function derivatives $a(s)\sqrt{2/k} \dot{A}_{\mu_1, \mu_2}(s, p)$ 447 momgrid : int 448 the grid size for the momentum integrals. 449 450 Returns 451 ------- 452 PTind : float 453 the gauge-field induced tensor power spectrum 454 """ 455 456 cutUV = self._khN(Ngrid[ind])/k 457 cutIR = min(pgrid)/k 458 HN = self._HN(Ngrid) 459 460 logAs = np.linspace(np.log(max(0.5, cutIR)), np.log(cutUV), momgrid) 461 462 Ax_rad = PchipInterpolator(np.log(pgrid), abs(A1)) 463 Ax_phase = PchipInterpolator(np.log(pgrid), np.arccos(A1.real/abs(A1))) 464 465 dAx_rad = PchipInterpolator(np.log(pgrid), abs(dA1)) 466 dAx_phase = PchipInterpolator(np.log(pgrid), np.arccos(dA1.real/abs(dA1))) 467 468 Ay_rad = PchipInterpolator(np.log(pgrid), abs(A2)) 469 Ay_phase = PchipInterpolator(np.log(pgrid), np.arccos(A2.real/abs(A2))) 470 471 dAy_rad = PchipInterpolator(np.log(pgrid), abs(dA2)) 472 dAy_phase = PchipInterpolator(np.log(pgrid), np.arccos(dA2.real/abs(dA2))) 473 474 def Afuncx(x): return Ax_rad(x)*np.exp(1j*Ax_phase(x)) 475 def dAfuncx(x): return dAx_rad(x)*np.exp(1j*dAx_phase(x)) 476 477 def Afuncy(x): return Ay_rad(x)*np.exp(1j*Ay_phase(x)) 478 def dAfuncy(x): return dAy_rad(x)*np.exp(1j*dAy_phase(x)) 479 480 IntOuter = [] 481 for logA in logAs: 482 A = np.exp(logA) 483 Blow = (cutIR - A) 484 Bhigh = (A - cutIR) 485 if Bhigh>0.5: 486 Blow = (A - cutUV) 487 Bhigh = (cutUV - A) 488 if Bhigh>0.5: 489 Blow = -0.5 490 Bhigh = 0.5 491 Bs = np.linspace(Blow, Bhigh, momgrid)[1:-1] 492 493 IntInner = np.zeros(Bs.shape) 494 495 for j, B in enumerate(Bs): 496 Ax = Afuncx(np.log(k*(A+B))) 497 dAx = dAfuncx(np.log(k*(A+B))) 498 Ay = Afuncy(np.log(k*(A-B))) 499 dAy = dAfuncy(np.log(k*(A-B))) 500 501 mom = abs( l1*l2 + 2*lgrav*( (l1+l2)*A + (l1-l2)*B ) + 4*(A**2 - B**2) + 8*lgrav*A*B*( (l1-l2)*A - (l1+l2)*B ) - 16*l1*l2*A**2*B**2 ) 502 z = max(A+B,A-B) 503 mask = np.where(z<self._khN(Ngrid)/k, 1, 0) 504 505 val = (dAx*dAy + l1*l2*Ax*Ay)*mask*k/(np.exp(3*Ngrid)*HN) 506 timeintegrand = GreenN*val*mom 507 508 timeintre = trapezoid(timeintegrand[:ind].real, Ngrid[:ind]) 509 510 timeintim = trapezoid(timeintegrand[:ind].imag, Ngrid[:ind]) 511 512 IntInner[j] = (timeintre**2 + timeintim**2)*A 513 514 515 IntOuter.append(trapezoid(IntInner, Bs)) 516 IntOuter = np.array(IntOuter) 517 518 PTind = trapezoid(IntOuter, logAs) / (16*np.pi**4)*(k*self._omega)**4 519 520 return PTind 521
15class PowSpecT: 16 r""" 17 A class used to compute the tensor power spectrum including vacuum and gauge-field induced contributions. 18 19 These main method of this module is `compute_pt`, which computes both the vacuum and gauge-field induced contribution 20 to the tensor power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}$ and $\mathcal{P}_{T,\lambda}^{\mathrm{ind}}$. 21 22 Results are internally computed using numerical units, but are returned in physical units. 23 """ 24 def __init__(self, insys : BGSystem): 25 """ 26 Initialise the class from a GEF solution. 27 28 Parameters 29 ---------- 30 insys : BGSystem 31 the GEF solution. 32 """ 33 #Set GEF results to Hubble units. 34 sys = BGSystem.from_system(insys, True) 35 sys.units = False 36 37 #import the background evolution 38 N = sys.N.value 39 a = np.exp(N) 40 H = sys.H.value 41 42 self._omega = sys.omega 43 44 #Set the range of modes 45 self.maxk = max(a*H) 46 self.mink = 1e4 47 48 #Define Useful quantities 49 50 self._t = sys.t.value 51 self._N = N 52 self._H = H 53 self._xi= sys.xi.value 54 55 #define interpolated quantities as needed 56 self._af = CubicSpline(self._t, a) 57 if isinstance(self._H, Constant): 58 self._Hf = lambda t: self._H 59 else: 60 self._Hf = CubicSpline(self._t, H) 61 self._HN = CubicSpline(self._N, self._H) 62 self._khN = CubicSpline(N, sys.kh) 63 64 #Obtain eta as a functio of time for phases 65 def deta(t, y): return 1/self._af(t) 66 67 soleta = solve_ivp(deta, [min(self._t), max(self._t)], np.array([0]), t_eval=self._t) 68 69 self._etaf = CubicSpline(self._t, soleta.y[0,:]) 70 return 71 72 def compute_pt(self, nmodes : int, spec : GaugeSpec, FastGW : bool=True, 73 atols : list=[1e-3,1e-20], rtols : list=[1e-4,1e-4], momgrid : int=100 74 ) -> Tuple[np.ndarray,dict]: 75 r""" 76 Compute the full tensor power spectrum. 77 78 The method lodes data on gauge modes $A_\lambda(t,k)$ from a file indicated by `mbm_file`. 79 The power spectrum is evaluated fpr `nmodes` log-spaced momenta $k \in [10^{4}a(0)H(0), 10 a(t_{\rm max}) H(t_{\rm max})]$ 80 81 Parameters 82 ---------- 83 nmodes : NDArray 84 the number of momenta 85 ModePath : str 86 path to a file containing gauge-field mode functions 87 FastGW : bool 88 If `True`, only the expected dominant contributions to $\mathcal{P}_{T,\lambda}^{\mathrm{ind}}$ are computed 89 atols : list 90 absolute tolerance for `compute_homogeneous` (index 0) and `compute_green` (index 1) 91 rtols : list 92 relative tolerance for `compute_homogeneous` (index 0) and `compute_green` (index 1) 93 momgrid : int 94 passed to `compute_ptind` 95 96 Returns 97 ------- 98 ks : NDArray 99 the momenta $k$ 100 PT : dict 101 a dictionary containing all contributions to the tensor power spectrum and its total. 102 """ 103 104 k = np.logspace(np.log10(self.mink), np.log10(10*self.maxk), nmodes) 105 ks, tstarts = self._find_tinit_bd(k, mode="k") 106 107 Ngrid = spec["N"] 108 tgrid = spec["t"] 109 pgrid = spec["k"] 110 111 GaugeModes = {"+":(spec["Ap"], spec["dAp"]), "-":(spec["Am"], spec["dAm"])} 112 113 indend = np.argmin(abs(Ngrid-max(self._N))) 114 115 PT = {"tot":[], "vac":[], "ind+,++":[], "ind+,+-":[], "ind+,--":[], "ind-,++":[], "ind-,+-":[], "ind-,--":[]} 116 117 GWpols = [("+", 1), ("-",-1)] 118 119 gaugepols=[(("+",1),("+",1)), 120 (("+",1),("-",-1)), 121 (("-",-1),("-",-1))] 122 123 sign = np.sign(self._xi[0]) 124 125 for i, k in enumerate(ks): 126 tstart = tstarts[i] 127 128 if k > 10**(5/2)*self.maxk: 129 for key in PT.keys(): 130 PT[key].append(0) 131 else: 132 uk, _ = self.compute_homogeneous(k, tstart, tgrid, atol=atols[0], rtol=rtols[0]) 133 Green = self.compute_green(k, uk, tstart, tgrid, indend, atol=atols[1], rtol=rtols[1]) 134 135 PT["vac"].append(self.compute_ptvac(k, uk[indend])) 136 137 for lgrav in GWpols: 138 for mu in gaugepols: 139 GWpol = lgrav[1] 140 141 Ax = GaugeModes[mu[0][0]][0] 142 dAx = GaugeModes[mu[0][0]][1] 143 lx = mu[0][1] 144 Ay = GaugeModes[mu[1][0]][0] 145 dAy = GaugeModes[mu[1][0]][1] 146 ly = mu[1][1] 147 148 if FastGW and (lx != sign or ly != sign): 149 PT[f"ind{lgrav[0]},{mu[0][0]}{mu[1][0]}"].append(0.) 150 else: 151 PT[f"ind{lgrav[0]},{mu[0][0]}{mu[1][0]}"].append( 152 self.compute_ptind(k, GWpol, Ngrid, indend, Green, pgrid, lx, Ax, dAx, ly, Ay, dAy, momgrid) ) 153 154 PT["tot"] = np.zeros(ks.shape) 155 for key in PT.keys(): 156 PT[key] = np.array(PT[key]) 157 if ("+" in key) or ("-" in key): 158 PT["tot"] += 0.5*PT[key] 159 elif key=="vac": 160 PT["tot"] += PT[key] 161 162 #this returns k with units restored. Needs to be matched by omega_GW 163 return ks*self._omega, PT 164 165 def compute_homogeneous(self, k : float, tvac : float, teval : np.ndarray|NoneType, atol : float=1e-3, rtol : float=1e-4) -> Tuple[np.ndarray, np.ndarray]: 166 r""" 167 Compute the evolution of a vacuum mode starting from Bunch–Davies vacuum. 168 169 For a mode $u_0(t,k)$ with momentum $k$, initialise in Bunch–Davies when $k = 10^{5/2} a(t_{\rm vac})H(t_{\rm vac})$. 170 Its evolution is obtained by solving `tensor_mode_eq` using `scipy.integrate.solve_ivp`. 171 172 Parameters 173 ---------- 174 k : float 175 the momentum $k$. 176 tvac : float 177 the initial time, $t_{\rm init}$ 178 teval : NDArray or None 179 time coordinates $t$ of $u_0(t,k)$ (`None`: same as class input `sys`) 180 atol : float 181 the absolute tolerance for `solve_ivp` 182 rtol : float 183 the relative tolerance for `solve_ivp` 184 185 Returns 186 ------- 187 u : NDArray 188 the vacuum tensor modes, $\sqrt{2k}u(t, k)$ 189 duk : NDArray 190 the derivative of the vacuum tensor modes, $a\sqrt{2/k}\dot{u}(t, k)$ 191 """ 192 193 if len(teval)==0: 194 teval = self._t 195 tend = max(teval) 196 197 #conformal time needed for relative phases 198 eta = self._etaf(teval) 199 200 istart = 0 201 while teval[istart]<tvac: 202 istart+=1 203 204 #define the ODE for the GW modes 205 def ode(t, y): return self.tensor_mode_eq( y, k, self._Hf(t), self._af(t) ) 206 207 #Initialise the modes in Bunch Davies 208 Zini = np.array([1, -10**(-5/2), 0, -1]) 209 210 sol = solve_ivp(ode, [tvac, tend], Zini, t_eval=teval[istart:], method="RK45", atol=atol, rtol=rtol) 211 if not(sol.success): 212 print("Something went wrong") 213 214 #the mode was in vacuum before tvac 215 vac = list( np.exp(-1j*eta[:istart]*k) ) 216 dvac = list( -1j*np.exp(-1j*eta[:istart]*k) ) 217 218 #Create an array tracking a modes evolution from Bunch Davies to late times. Ensure equal length arrays for every mode k 219 uk = np.array( vac + list( (sol.y[0,:] + 1j*sol.y[2,:])*np.exp(-1j*k*eta[istart]) ) )/self._af(teval) 220 duk = np.array( dvac + list( (sol.y[1,:] + 1j*sol.y[3,:])*np.exp(-1j*k*eta[istart]) ) )/self._af(teval) 221 222 return uk, duk 223 224 @staticmethod 225 def tensor_mode_eq(y : np.ndarray, k : float, H : float, a : float) -> np.ndarray: 226 r""" 227 Mode equation for vacuum tensor modes. 228 229 Parameters 230 ---------- 231 y : NDArray 232 the vacuum tensor mode and its derivatives. 233 $a \sqrt{2k} u_0(t,k)$ and $ a^2\sqrt{2/k} \dot{u}_0(t, k)$ 234 k : float 235 comoving momentum $k$ 236 H : float 237 Hubble rate, $H(t)$ 238 a : float 239 scale factor, $a(t)$ 240 241 Returns 242 ------- 243 dydt : NDArray 244 an array of time derivatives of y 245 246 """ 247 dydt = np.zeros(y.shape) 248 249 #real 250 dydt[0] = (H*y[0] + k/a*y[1]) 251 dydt[1] = ( -H*y[1] - k/a*y[0] ) 252 253 #imaginary 254 dydt[2] = (H*y[2] + k/a*y[3]) 255 dydt[3] = ( -H*y[3] - k/a*y[2] ) 256 257 return dydt 258 259 def compute_green(self, k : float, uk : np.ndarray, tvac : float, teval : np.ndarray|NoneType, ind : int, atol : float=1e-30, rtol : float=1e-4) -> np.ndarray: 260 r""" 261 Compute the evolution of the Green function $G_k(t',t)$ for fixed $t'$. 262 263 The evolution is obtained by solving `green_ode` from $t=t'$ backwards until $t_{\rm vac}$, defined through $k = 10^{5/2} a(t_{\rm end})H(t_{\rm vac})$. 264 From $t_{\rm end}$ onwards, the Green function is instead computed from the mode $u_0(t, k)$. The method uses `scipy.integrate.solve_ivp`. 265 266 Parameters 267 ---------- 268 k : float 269 the momentum $k$. 270 uk : NDArray 271 the mode function $u_0(t,k)$ 272 tvac : float 273 the final time, $t_{\rm vac}$ 274 teval : NDArray or None 275 time coordinates $t$ of $G_k(t',t)$ (`None`: same as class input `sys`) 276 ind : int 277 index of teval corresponding to $t'$ 278 atol : float 279 the absolute tolerance for `solve_ivp` 280 rtol : float 281 the relative tolerance for `solve_ivp` 282 283 Return 284 ------ 285 GreenN : NDArray 286 the Green function $-k G_k(t', t)$ 287 """ 288 if len(teval)==0: 289 teval = self._t 290 291 istart = 0 292 while teval[istart]<tvac: 293 istart+=1 294 295 # G(k, t, t) = 0 by definition 296 Aini = np.array([0, 1]) 297 298 299 # Solve the EoM for G backwards in time starting from G(k, t, t) 300 def Aode(t, y): return -self.green_ode(y, k, self._Hf(-t), self._af(-t)) 301 solA = solve_ivp(Aode, [-teval[ind], -tvac], Aini, t_eval=-teval[istart:ind+1][::-1], 302 method="RK45", atol=atol, rtol=rtol) 303 304 #For numerical stability, only solve the EoM for the Green function until t' = tvac. Afterwards, compute it directly from the vacuum modes. 305 GreenN = np.zeros(teval.shape) 306 GreenN[istart:ind+1] = solA.y[0,:][::-1] 307 GreenN[:istart] = ( (uk[ind].conjugate()*uk).imag*self._af(teval)**2 )[:istart] 308 309 return GreenN 310 311 @staticmethod 312 def green_ode(y : np.ndarray, k : float, H : float, a : float) -> np.ndarray: 313 r""" 314 Evolve the re-scaled Green function for tensor modes in time. 315 316 The re-scaled Green function is 317 $$B_{t'}^k(t) = 2 k a(t)^2 \operatorname{Im} \left[ u_0^*(t', k) \, u_0(t,k) \right] = k G_k(t', t) \,.$$ 318 319 whose ODE is coupled to 320 321 $$C_{t'}^k(t) = 2 a(t)^3 \operatorname{Im} \left[ u_0^*(t', k) \, {\dot{u}_0}(t,k) \right] \,.$$ 322 323 Parameters 324 ---------- 325 y : NDArray 326 the functions B and C 327 k : float 328 comoving momentum $k$ 329 H : float 330 Hubble rate, $H(t)$ 331 a : float 332 scale factor, $a(t)$ 333 334 Returns 335 ------- 336 dydt : NDArray 337 an array of time derivatives of A 338 339 """ 340 dydt = np.zeros(y.shape) 341 dydt[0] =(2*H*y[0] + k/a*y[1]) 342 dydt[1] = -k/a*y[0] 343 return dydt 344 345 346 def _find_tinit_bd(self, init : np.ndarray, mode : str="k") -> Tuple[np.ndarray, np.ndarray]: 347 """ 348 Determines the pair of $k$ and $t$ satisfying $k = 10^(5/2)a(t)H(t)$. 349 350 Depending on `mode`, `init` may be a time coordinate (`mode='t'`), $e$-folds (`mode='N'`) or momentum (`mode='k'`). 351 352 Parameters 353 ---------- 354 init : array 355 the input array (t, N, or k) 356 mode : str 357 indicate the type of `init` 358 359 Returns 360 ------- 361 ks : NDarray 362 an array of momenta 363 tstart : NDarray 364 an array of times 365 366 Raises 367 ------ 368 KeyError 369 if `mode` is not 't, 'k' or 'N' 370 """ 371 372 pwr = 5/2 373 374 t = self._t 375 def logkH(t): return np.log(self._af(t)*self._Hf(t)) 376 if mode=="t": 377 tstart = init 378 logks = logkH(tstart) 379 ks = 10**(pwr)*np.exp(logks) 380 381 elif mode=="k": 382 ks = init 383 384 tstart = [] 385 for k in ks: 386 ttmp = self._t[np.searchsorted(10**(pwr)*np.exp(logkH(self._t)), k, "right")] 387 tstart.append(ttmp) 388 tstart = np.array(tstart) 389 390 elif mode=="N": 391 tstart = CubicSpline(self._N, t)(init) 392 ks = 10**pwr*np.exp(logkH(tstart)) 393 394 else: 395 raise KeyError("'mode' must be 't', 'k' or 'N'") 396 397 return ks, tstart 398 399 def compute_ptvac(self, k : np.ndarray, uk : np.ndarray) -> np.ndarray: 400 r""" 401 Compute the vacuum power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}(t,k)$. 402 403 Parameters 404 ---------- 405 k : NDArray 406 momentum, $k$ 407 uk : NDArray 408 vacuum tensor mode $u_0(t,k)$ 409 410 Return 411 ------ 412 PTvac : NDArray 413 the vacuum tensor power spectrum 414 """ 415 PTvac = 2*(k*self._omega)**2/( np.pi**2 ) * abs(uk)**2 416 return PTvac 417 418 def compute_ptind(self, k : float, lgrav : float, Ngrid : np.ndarray, ind: int, GreenN : np.ndarray, pgrid : np.ndarray, 419 l1 : float, A1 : np.ndarray, dA1 : np.ndarray, l2 : float, A2 : np.ndarray, dA2 : np.ndarray, 420 momgrid : int=100) -> float: 421 r""" 422 Compute the vacuum power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}(t,k)$. 423 424 The integral is computed by integrating over the inner integral over the gauge-mode functions $A_\mu(s,p)$ using `scipy.integrate.trapezoid`. 425 The external momentum integrals are also computed using `scipy.integrate.trapezoid` on a grid of momenta momgrid x momgrid. The mode functions 426 are interpolated over momentum to match this grid. 427 428 Parameters 429 ---------- 430 k : float 431 momentum $k$ 432 lgrav : NDArray 433 helicity $\lambda$ 434 Ngrid : NDArray 435 $e$-folds $\log a$ 436 ind : integer 437 the power spectrum is computed at $t$ corresponding to `Ngrid[ind]` 438 GreenN : NDArray 439 the Green function $kG_k(t, s)$ with $s$ corresponding to `Ngrid` 440 pgrid : NDArray 441 momenta $p$ for the mode functions $A_\mu(s,p)$ 442 l1, l2 : float 443 the gauge-field helicities $\mu_1$, $\mu_2$ 444 A1, A2 : float 445 the gauge-field mode functions $\sqrt{2k} A_{\mu_1, \mu_2}(s, p)$ 446 dA1, dA2 : float 447 the gauge-field mode function derivatives $a(s)\sqrt{2/k} \dot{A}_{\mu_1, \mu_2}(s, p)$ 448 momgrid : int 449 the grid size for the momentum integrals. 450 451 Returns 452 ------- 453 PTind : float 454 the gauge-field induced tensor power spectrum 455 """ 456 457 cutUV = self._khN(Ngrid[ind])/k 458 cutIR = min(pgrid)/k 459 HN = self._HN(Ngrid) 460 461 logAs = np.linspace(np.log(max(0.5, cutIR)), np.log(cutUV), momgrid) 462 463 Ax_rad = PchipInterpolator(np.log(pgrid), abs(A1)) 464 Ax_phase = PchipInterpolator(np.log(pgrid), np.arccos(A1.real/abs(A1))) 465 466 dAx_rad = PchipInterpolator(np.log(pgrid), abs(dA1)) 467 dAx_phase = PchipInterpolator(np.log(pgrid), np.arccos(dA1.real/abs(dA1))) 468 469 Ay_rad = PchipInterpolator(np.log(pgrid), abs(A2)) 470 Ay_phase = PchipInterpolator(np.log(pgrid), np.arccos(A2.real/abs(A2))) 471 472 dAy_rad = PchipInterpolator(np.log(pgrid), abs(dA2)) 473 dAy_phase = PchipInterpolator(np.log(pgrid), np.arccos(dA2.real/abs(dA2))) 474 475 def Afuncx(x): return Ax_rad(x)*np.exp(1j*Ax_phase(x)) 476 def dAfuncx(x): return dAx_rad(x)*np.exp(1j*dAx_phase(x)) 477 478 def Afuncy(x): return Ay_rad(x)*np.exp(1j*Ay_phase(x)) 479 def dAfuncy(x): return dAy_rad(x)*np.exp(1j*dAy_phase(x)) 480 481 IntOuter = [] 482 for logA in logAs: 483 A = np.exp(logA) 484 Blow = (cutIR - A) 485 Bhigh = (A - cutIR) 486 if Bhigh>0.5: 487 Blow = (A - cutUV) 488 Bhigh = (cutUV - A) 489 if Bhigh>0.5: 490 Blow = -0.5 491 Bhigh = 0.5 492 Bs = np.linspace(Blow, Bhigh, momgrid)[1:-1] 493 494 IntInner = np.zeros(Bs.shape) 495 496 for j, B in enumerate(Bs): 497 Ax = Afuncx(np.log(k*(A+B))) 498 dAx = dAfuncx(np.log(k*(A+B))) 499 Ay = Afuncy(np.log(k*(A-B))) 500 dAy = dAfuncy(np.log(k*(A-B))) 501 502 mom = abs( l1*l2 + 2*lgrav*( (l1+l2)*A + (l1-l2)*B ) + 4*(A**2 - B**2) + 8*lgrav*A*B*( (l1-l2)*A - (l1+l2)*B ) - 16*l1*l2*A**2*B**2 ) 503 z = max(A+B,A-B) 504 mask = np.where(z<self._khN(Ngrid)/k, 1, 0) 505 506 val = (dAx*dAy + l1*l2*Ax*Ay)*mask*k/(np.exp(3*Ngrid)*HN) 507 timeintegrand = GreenN*val*mom 508 509 timeintre = trapezoid(timeintegrand[:ind].real, Ngrid[:ind]) 510 511 timeintim = trapezoid(timeintegrand[:ind].imag, Ngrid[:ind]) 512 513 IntInner[j] = (timeintre**2 + timeintim**2)*A 514 515 516 IntOuter.append(trapezoid(IntInner, Bs)) 517 IntOuter = np.array(IntOuter) 518 519 PTind = trapezoid(IntOuter, logAs) / (16*np.pi**4)*(k*self._omega)**4 520 521 return PTind
A class used to compute the tensor power spectrum including vacuum and gauge-field induced contributions.
These main method of this module is compute_pt, which computes both the vacuum and gauge-field induced contribution
to the tensor power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}$ and $\mathcal{P}_{T,\lambda}^{\mathrm{ind}}$.
Results are internally computed using numerical units, but are returned in physical units.
24 def __init__(self, insys : BGSystem): 25 """ 26 Initialise the class from a GEF solution. 27 28 Parameters 29 ---------- 30 insys : BGSystem 31 the GEF solution. 32 """ 33 #Set GEF results to Hubble units. 34 sys = BGSystem.from_system(insys, True) 35 sys.units = False 36 37 #import the background evolution 38 N = sys.N.value 39 a = np.exp(N) 40 H = sys.H.value 41 42 self._omega = sys.omega 43 44 #Set the range of modes 45 self.maxk = max(a*H) 46 self.mink = 1e4 47 48 #Define Useful quantities 49 50 self._t = sys.t.value 51 self._N = N 52 self._H = H 53 self._xi= sys.xi.value 54 55 #define interpolated quantities as needed 56 self._af = CubicSpline(self._t, a) 57 if isinstance(self._H, Constant): 58 self._Hf = lambda t: self._H 59 else: 60 self._Hf = CubicSpline(self._t, H) 61 self._HN = CubicSpline(self._N, self._H) 62 self._khN = CubicSpline(N, sys.kh) 63 64 #Obtain eta as a functio of time for phases 65 def deta(t, y): return 1/self._af(t) 66 67 soleta = solve_ivp(deta, [min(self._t), max(self._t)], np.array([0]), t_eval=self._t) 68 69 self._etaf = CubicSpline(self._t, soleta.y[0,:]) 70 return
Initialise the class from a GEF solution.
Parameters
- insys (BGSystem): the GEF solution.
72 def compute_pt(self, nmodes : int, spec : GaugeSpec, FastGW : bool=True, 73 atols : list=[1e-3,1e-20], rtols : list=[1e-4,1e-4], momgrid : int=100 74 ) -> Tuple[np.ndarray,dict]: 75 r""" 76 Compute the full tensor power spectrum. 77 78 The method lodes data on gauge modes $A_\lambda(t,k)$ from a file indicated by `mbm_file`. 79 The power spectrum is evaluated fpr `nmodes` log-spaced momenta $k \in [10^{4}a(0)H(0), 10 a(t_{\rm max}) H(t_{\rm max})]$ 80 81 Parameters 82 ---------- 83 nmodes : NDArray 84 the number of momenta 85 ModePath : str 86 path to a file containing gauge-field mode functions 87 FastGW : bool 88 If `True`, only the expected dominant contributions to $\mathcal{P}_{T,\lambda}^{\mathrm{ind}}$ are computed 89 atols : list 90 absolute tolerance for `compute_homogeneous` (index 0) and `compute_green` (index 1) 91 rtols : list 92 relative tolerance for `compute_homogeneous` (index 0) and `compute_green` (index 1) 93 momgrid : int 94 passed to `compute_ptind` 95 96 Returns 97 ------- 98 ks : NDArray 99 the momenta $k$ 100 PT : dict 101 a dictionary containing all contributions to the tensor power spectrum and its total. 102 """ 103 104 k = np.logspace(np.log10(self.mink), np.log10(10*self.maxk), nmodes) 105 ks, tstarts = self._find_tinit_bd(k, mode="k") 106 107 Ngrid = spec["N"] 108 tgrid = spec["t"] 109 pgrid = spec["k"] 110 111 GaugeModes = {"+":(spec["Ap"], spec["dAp"]), "-":(spec["Am"], spec["dAm"])} 112 113 indend = np.argmin(abs(Ngrid-max(self._N))) 114 115 PT = {"tot":[], "vac":[], "ind+,++":[], "ind+,+-":[], "ind+,--":[], "ind-,++":[], "ind-,+-":[], "ind-,--":[]} 116 117 GWpols = [("+", 1), ("-",-1)] 118 119 gaugepols=[(("+",1),("+",1)), 120 (("+",1),("-",-1)), 121 (("-",-1),("-",-1))] 122 123 sign = np.sign(self._xi[0]) 124 125 for i, k in enumerate(ks): 126 tstart = tstarts[i] 127 128 if k > 10**(5/2)*self.maxk: 129 for key in PT.keys(): 130 PT[key].append(0) 131 else: 132 uk, _ = self.compute_homogeneous(k, tstart, tgrid, atol=atols[0], rtol=rtols[0]) 133 Green = self.compute_green(k, uk, tstart, tgrid, indend, atol=atols[1], rtol=rtols[1]) 134 135 PT["vac"].append(self.compute_ptvac(k, uk[indend])) 136 137 for lgrav in GWpols: 138 for mu in gaugepols: 139 GWpol = lgrav[1] 140 141 Ax = GaugeModes[mu[0][0]][0] 142 dAx = GaugeModes[mu[0][0]][1] 143 lx = mu[0][1] 144 Ay = GaugeModes[mu[1][0]][0] 145 dAy = GaugeModes[mu[1][0]][1] 146 ly = mu[1][1] 147 148 if FastGW and (lx != sign or ly != sign): 149 PT[f"ind{lgrav[0]},{mu[0][0]}{mu[1][0]}"].append(0.) 150 else: 151 PT[f"ind{lgrav[0]},{mu[0][0]}{mu[1][0]}"].append( 152 self.compute_ptind(k, GWpol, Ngrid, indend, Green, pgrid, lx, Ax, dAx, ly, Ay, dAy, momgrid) ) 153 154 PT["tot"] = np.zeros(ks.shape) 155 for key in PT.keys(): 156 PT[key] = np.array(PT[key]) 157 if ("+" in key) or ("-" in key): 158 PT["tot"] += 0.5*PT[key] 159 elif key=="vac": 160 PT["tot"] += PT[key] 161 162 #this returns k with units restored. Needs to be matched by omega_GW 163 return ks*self._omega, PT
Compute the full tensor power spectrum.
The method lodes data on gauge modes $A_\lambda(t,k)$ from a file indicated by mbm_file.
The power spectrum is evaluated fpr nmodes log-spaced momenta $k \in [10^{4}a(0)H(0), 10 a(t_{\rm max}) H(t_{\rm max})]$
Parameters
- nmodes (NDArray): the number of momenta
- ModePath (str): path to a file containing gauge-field mode functions
- FastGW (bool):
If
True, only the expected dominant contributions to $\mathcal{P}_{T,\lambda}^{\mathrm{ind}}$ are computed - atols (list):
absolute tolerance for
compute_homogeneous(index 0) andcompute_green(index 1) - rtols (list):
relative tolerance for
compute_homogeneous(index 0) andcompute_green(index 1) - momgrid (int):
passed to
compute_ptind
Returns
- ks (NDArray): the momenta $k$
- PT (dict): a dictionary containing all contributions to the tensor power spectrum and its total.
165 def compute_homogeneous(self, k : float, tvac : float, teval : np.ndarray|NoneType, atol : float=1e-3, rtol : float=1e-4) -> Tuple[np.ndarray, np.ndarray]: 166 r""" 167 Compute the evolution of a vacuum mode starting from Bunch–Davies vacuum. 168 169 For a mode $u_0(t,k)$ with momentum $k$, initialise in Bunch–Davies when $k = 10^{5/2} a(t_{\rm vac})H(t_{\rm vac})$. 170 Its evolution is obtained by solving `tensor_mode_eq` using `scipy.integrate.solve_ivp`. 171 172 Parameters 173 ---------- 174 k : float 175 the momentum $k$. 176 tvac : float 177 the initial time, $t_{\rm init}$ 178 teval : NDArray or None 179 time coordinates $t$ of $u_0(t,k)$ (`None`: same as class input `sys`) 180 atol : float 181 the absolute tolerance for `solve_ivp` 182 rtol : float 183 the relative tolerance for `solve_ivp` 184 185 Returns 186 ------- 187 u : NDArray 188 the vacuum tensor modes, $\sqrt{2k}u(t, k)$ 189 duk : NDArray 190 the derivative of the vacuum tensor modes, $a\sqrt{2/k}\dot{u}(t, k)$ 191 """ 192 193 if len(teval)==0: 194 teval = self._t 195 tend = max(teval) 196 197 #conformal time needed for relative phases 198 eta = self._etaf(teval) 199 200 istart = 0 201 while teval[istart]<tvac: 202 istart+=1 203 204 #define the ODE for the GW modes 205 def ode(t, y): return self.tensor_mode_eq( y, k, self._Hf(t), self._af(t) ) 206 207 #Initialise the modes in Bunch Davies 208 Zini = np.array([1, -10**(-5/2), 0, -1]) 209 210 sol = solve_ivp(ode, [tvac, tend], Zini, t_eval=teval[istart:], method="RK45", atol=atol, rtol=rtol) 211 if not(sol.success): 212 print("Something went wrong") 213 214 #the mode was in vacuum before tvac 215 vac = list( np.exp(-1j*eta[:istart]*k) ) 216 dvac = list( -1j*np.exp(-1j*eta[:istart]*k) ) 217 218 #Create an array tracking a modes evolution from Bunch Davies to late times. Ensure equal length arrays for every mode k 219 uk = np.array( vac + list( (sol.y[0,:] + 1j*sol.y[2,:])*np.exp(-1j*k*eta[istart]) ) )/self._af(teval) 220 duk = np.array( dvac + list( (sol.y[1,:] + 1j*sol.y[3,:])*np.exp(-1j*k*eta[istart]) ) )/self._af(teval) 221 222 return uk, duk
Compute the evolution of a vacuum mode starting from Bunch–Davies vacuum.
For a mode $u_0(t,k)$ with momentum $k$, initialise in Bunch–Davies when $k = 10^{5/2} a(t_{\rm vac})H(t_{\rm vac})$.
Its evolution is obtained by solving tensor_mode_eq using scipy.integrate.solve_ivp.
Parameters
- k (float): the momentum $k$.
- tvac (float): the initial time, $t_{\rm init}$
- teval (NDArray or None):
time coordinates $t$ of $u_0(t,k)$ (
None: same as class inputsys) - atol (float):
the absolute tolerance for
solve_ivp - rtol (float):
the relative tolerance for
solve_ivp
Returns
- u (NDArray): the vacuum tensor modes, $\sqrt{2k}u(t, k)$
- duk (NDArray): the derivative of the vacuum tensor modes, $a\sqrt{2/k}\dot{u}(t, k)$
224 @staticmethod 225 def tensor_mode_eq(y : np.ndarray, k : float, H : float, a : float) -> np.ndarray: 226 r""" 227 Mode equation for vacuum tensor modes. 228 229 Parameters 230 ---------- 231 y : NDArray 232 the vacuum tensor mode and its derivatives. 233 $a \sqrt{2k} u_0(t,k)$ and $ a^2\sqrt{2/k} \dot{u}_0(t, k)$ 234 k : float 235 comoving momentum $k$ 236 H : float 237 Hubble rate, $H(t)$ 238 a : float 239 scale factor, $a(t)$ 240 241 Returns 242 ------- 243 dydt : NDArray 244 an array of time derivatives of y 245 246 """ 247 dydt = np.zeros(y.shape) 248 249 #real 250 dydt[0] = (H*y[0] + k/a*y[1]) 251 dydt[1] = ( -H*y[1] - k/a*y[0] ) 252 253 #imaginary 254 dydt[2] = (H*y[2] + k/a*y[3]) 255 dydt[3] = ( -H*y[3] - k/a*y[2] ) 256 257 return dydt
Mode equation for vacuum tensor modes.
Parameters
- y (NDArray): the vacuum tensor mode and its derivatives. $a \sqrt{2k} u_0(t,k)$ and $ a^2\sqrt{2/k} \dot{u}_0(t, k)$
- k (float): comoving momentum $k$
- H (float): Hubble rate, $H(t)$
- a (float): scale factor, $a(t)$
Returns
- dydt (NDArray): an array of time derivatives of y
259 def compute_green(self, k : float, uk : np.ndarray, tvac : float, teval : np.ndarray|NoneType, ind : int, atol : float=1e-30, rtol : float=1e-4) -> np.ndarray: 260 r""" 261 Compute the evolution of the Green function $G_k(t',t)$ for fixed $t'$. 262 263 The evolution is obtained by solving `green_ode` from $t=t'$ backwards until $t_{\rm vac}$, defined through $k = 10^{5/2} a(t_{\rm end})H(t_{\rm vac})$. 264 From $t_{\rm end}$ onwards, the Green function is instead computed from the mode $u_0(t, k)$. The method uses `scipy.integrate.solve_ivp`. 265 266 Parameters 267 ---------- 268 k : float 269 the momentum $k$. 270 uk : NDArray 271 the mode function $u_0(t,k)$ 272 tvac : float 273 the final time, $t_{\rm vac}$ 274 teval : NDArray or None 275 time coordinates $t$ of $G_k(t',t)$ (`None`: same as class input `sys`) 276 ind : int 277 index of teval corresponding to $t'$ 278 atol : float 279 the absolute tolerance for `solve_ivp` 280 rtol : float 281 the relative tolerance for `solve_ivp` 282 283 Return 284 ------ 285 GreenN : NDArray 286 the Green function $-k G_k(t', t)$ 287 """ 288 if len(teval)==0: 289 teval = self._t 290 291 istart = 0 292 while teval[istart]<tvac: 293 istart+=1 294 295 # G(k, t, t) = 0 by definition 296 Aini = np.array([0, 1]) 297 298 299 # Solve the EoM for G backwards in time starting from G(k, t, t) 300 def Aode(t, y): return -self.green_ode(y, k, self._Hf(-t), self._af(-t)) 301 solA = solve_ivp(Aode, [-teval[ind], -tvac], Aini, t_eval=-teval[istart:ind+1][::-1], 302 method="RK45", atol=atol, rtol=rtol) 303 304 #For numerical stability, only solve the EoM for the Green function until t' = tvac. Afterwards, compute it directly from the vacuum modes. 305 GreenN = np.zeros(teval.shape) 306 GreenN[istart:ind+1] = solA.y[0,:][::-1] 307 GreenN[:istart] = ( (uk[ind].conjugate()*uk).imag*self._af(teval)**2 )[:istart] 308 309 return GreenN
Compute the evolution of the Green function $G_k(t',t)$ for fixed $t'$.
The evolution is obtained by solving green_ode from $t=t'$ backwards until $t_{\rm vac}$, defined through $k = 10^{5/2} a(t_{\rm end})H(t_{\rm vac})$.
From $t_{\rm end}$ onwards, the Green function is instead computed from the mode $u_0(t, k)$. The method uses scipy.integrate.solve_ivp.
Parameters
- k (float): the momentum $k$.
- uk (NDArray): the mode function $u_0(t,k)$
- tvac (float): the final time, $t_{\rm vac}$
- teval (NDArray or None):
time coordinates $t$ of $G_k(t',t)$ (
None: same as class inputsys) - ind (int): index of teval corresponding to $t'$
- atol (float):
the absolute tolerance for
solve_ivp - rtol (float):
the relative tolerance for
solve_ivp
Return
GreenN : NDArray the Green function $-k G_k(t', t)$
311 @staticmethod 312 def green_ode(y : np.ndarray, k : float, H : float, a : float) -> np.ndarray: 313 r""" 314 Evolve the re-scaled Green function for tensor modes in time. 315 316 The re-scaled Green function is 317 $$B_{t'}^k(t) = 2 k a(t)^2 \operatorname{Im} \left[ u_0^*(t', k) \, u_0(t,k) \right] = k G_k(t', t) \,.$$ 318 319 whose ODE is coupled to 320 321 $$C_{t'}^k(t) = 2 a(t)^3 \operatorname{Im} \left[ u_0^*(t', k) \, {\dot{u}_0}(t,k) \right] \,.$$ 322 323 Parameters 324 ---------- 325 y : NDArray 326 the functions B and C 327 k : float 328 comoving momentum $k$ 329 H : float 330 Hubble rate, $H(t)$ 331 a : float 332 scale factor, $a(t)$ 333 334 Returns 335 ------- 336 dydt : NDArray 337 an array of time derivatives of A 338 339 """ 340 dydt = np.zeros(y.shape) 341 dydt[0] =(2*H*y[0] + k/a*y[1]) 342 dydt[1] = -k/a*y[0] 343 return dydt
Evolve the re-scaled Green function for tensor modes in time.
The re-scaled Green function is $$B_{t'}^k(t) = 2 k a(t)^2 \operatorname{Im} \left[ u_0^*(t', k) \, u_0(t,k) \right] = k G_k(t', t) \,.$$
whose ODE is coupled to
$$C_{t'}^k(t) = 2 a(t)^3 \operatorname{Im} \left[ u_0^*(t', k) \, {\dot{u}_0}(t,k) \right] \,.$$
Parameters
- y (NDArray): the functions B and C
- k (float): comoving momentum $k$
- H (float): Hubble rate, $H(t)$
- a (float): scale factor, $a(t)$
Returns
- dydt (NDArray): an array of time derivatives of A
399 def compute_ptvac(self, k : np.ndarray, uk : np.ndarray) -> np.ndarray: 400 r""" 401 Compute the vacuum power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}(t,k)$. 402 403 Parameters 404 ---------- 405 k : NDArray 406 momentum, $k$ 407 uk : NDArray 408 vacuum tensor mode $u_0(t,k)$ 409 410 Return 411 ------ 412 PTvac : NDArray 413 the vacuum tensor power spectrum 414 """ 415 PTvac = 2*(k*self._omega)**2/( np.pi**2 ) * abs(uk)**2 416 return PTvac
Compute the vacuum power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}(t,k)$.
Parameters
- k (NDArray): momentum, $k$
- uk (NDArray): vacuum tensor mode $u_0(t,k)$
Return
PTvac : NDArray the vacuum tensor power spectrum
418 def compute_ptind(self, k : float, lgrav : float, Ngrid : np.ndarray, ind: int, GreenN : np.ndarray, pgrid : np.ndarray, 419 l1 : float, A1 : np.ndarray, dA1 : np.ndarray, l2 : float, A2 : np.ndarray, dA2 : np.ndarray, 420 momgrid : int=100) -> float: 421 r""" 422 Compute the vacuum power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}(t,k)$. 423 424 The integral is computed by integrating over the inner integral over the gauge-mode functions $A_\mu(s,p)$ using `scipy.integrate.trapezoid`. 425 The external momentum integrals are also computed using `scipy.integrate.trapezoid` on a grid of momenta momgrid x momgrid. The mode functions 426 are interpolated over momentum to match this grid. 427 428 Parameters 429 ---------- 430 k : float 431 momentum $k$ 432 lgrav : NDArray 433 helicity $\lambda$ 434 Ngrid : NDArray 435 $e$-folds $\log a$ 436 ind : integer 437 the power spectrum is computed at $t$ corresponding to `Ngrid[ind]` 438 GreenN : NDArray 439 the Green function $kG_k(t, s)$ with $s$ corresponding to `Ngrid` 440 pgrid : NDArray 441 momenta $p$ for the mode functions $A_\mu(s,p)$ 442 l1, l2 : float 443 the gauge-field helicities $\mu_1$, $\mu_2$ 444 A1, A2 : float 445 the gauge-field mode functions $\sqrt{2k} A_{\mu_1, \mu_2}(s, p)$ 446 dA1, dA2 : float 447 the gauge-field mode function derivatives $a(s)\sqrt{2/k} \dot{A}_{\mu_1, \mu_2}(s, p)$ 448 momgrid : int 449 the grid size for the momentum integrals. 450 451 Returns 452 ------- 453 PTind : float 454 the gauge-field induced tensor power spectrum 455 """ 456 457 cutUV = self._khN(Ngrid[ind])/k 458 cutIR = min(pgrid)/k 459 HN = self._HN(Ngrid) 460 461 logAs = np.linspace(np.log(max(0.5, cutIR)), np.log(cutUV), momgrid) 462 463 Ax_rad = PchipInterpolator(np.log(pgrid), abs(A1)) 464 Ax_phase = PchipInterpolator(np.log(pgrid), np.arccos(A1.real/abs(A1))) 465 466 dAx_rad = PchipInterpolator(np.log(pgrid), abs(dA1)) 467 dAx_phase = PchipInterpolator(np.log(pgrid), np.arccos(dA1.real/abs(dA1))) 468 469 Ay_rad = PchipInterpolator(np.log(pgrid), abs(A2)) 470 Ay_phase = PchipInterpolator(np.log(pgrid), np.arccos(A2.real/abs(A2))) 471 472 dAy_rad = PchipInterpolator(np.log(pgrid), abs(dA2)) 473 dAy_phase = PchipInterpolator(np.log(pgrid), np.arccos(dA2.real/abs(dA2))) 474 475 def Afuncx(x): return Ax_rad(x)*np.exp(1j*Ax_phase(x)) 476 def dAfuncx(x): return dAx_rad(x)*np.exp(1j*dAx_phase(x)) 477 478 def Afuncy(x): return Ay_rad(x)*np.exp(1j*Ay_phase(x)) 479 def dAfuncy(x): return dAy_rad(x)*np.exp(1j*dAy_phase(x)) 480 481 IntOuter = [] 482 for logA in logAs: 483 A = np.exp(logA) 484 Blow = (cutIR - A) 485 Bhigh = (A - cutIR) 486 if Bhigh>0.5: 487 Blow = (A - cutUV) 488 Bhigh = (cutUV - A) 489 if Bhigh>0.5: 490 Blow = -0.5 491 Bhigh = 0.5 492 Bs = np.linspace(Blow, Bhigh, momgrid)[1:-1] 493 494 IntInner = np.zeros(Bs.shape) 495 496 for j, B in enumerate(Bs): 497 Ax = Afuncx(np.log(k*(A+B))) 498 dAx = dAfuncx(np.log(k*(A+B))) 499 Ay = Afuncy(np.log(k*(A-B))) 500 dAy = dAfuncy(np.log(k*(A-B))) 501 502 mom = abs( l1*l2 + 2*lgrav*( (l1+l2)*A + (l1-l2)*B ) + 4*(A**2 - B**2) + 8*lgrav*A*B*( (l1-l2)*A - (l1+l2)*B ) - 16*l1*l2*A**2*B**2 ) 503 z = max(A+B,A-B) 504 mask = np.where(z<self._khN(Ngrid)/k, 1, 0) 505 506 val = (dAx*dAy + l1*l2*Ax*Ay)*mask*k/(np.exp(3*Ngrid)*HN) 507 timeintegrand = GreenN*val*mom 508 509 timeintre = trapezoid(timeintegrand[:ind].real, Ngrid[:ind]) 510 511 timeintim = trapezoid(timeintegrand[:ind].imag, Ngrid[:ind]) 512 513 IntInner[j] = (timeintre**2 + timeintim**2)*A 514 515 516 IntOuter.append(trapezoid(IntInner, Bs)) 517 IntOuter = np.array(IntOuter) 518 519 PTind = trapezoid(IntOuter, logAs) / (16*np.pi**4)*(k*self._omega)**4 520 521 return PTind
Compute the vacuum power spectrum, $\mathcal{P}_{T,\lambda}^{\mathrm{vac}}(t,k)$.
The integral is computed by integrating over the inner integral over the gauge-mode functions $A_\mu(s,p)$ using scipy.integrate.trapezoid.
The external momentum integrals are also computed using scipy.integrate.trapezoid on a grid of momenta momgrid x momgrid. The mode functions
are interpolated over momentum to match this grid.
Parameters
- k (float): momentum $k$
- lgrav (NDArray): helicity $\lambda$
- Ngrid (NDArray): $e$-folds $\log a$
- ind (integer):
the power spectrum is computed at $t$ corresponding to
Ngrid[ind] - GreenN (NDArray):
the Green function $kG_k(t, s)$ with $s$ corresponding to
Ngrid - pgrid (NDArray): momenta $p$ for the mode functions $A_\mu(s,p)$
- l1, l2 (float): the gauge-field helicities $\mu_1$, $\mu_2$
- A1, A2 (float): the gauge-field mode functions $\sqrt{2k} A_{\mu_1, \mu_2}(s, p)$
- dA1, dA2 (float): the gauge-field mode function derivatives $a(s)\sqrt{2/k} \dot{A}_{\mu_1, \mu_2}(s, p)$
- momgrid (int): the grid size for the momentum integrals.
Returns
- PTind (float): the gauge-field induced tensor power spectrum