TransitionListener 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. transitionlistener/__init__.py +21 -0
  2. transitionlistener/bubbledynamics.py +2250 -0
  3. transitionlistener/bubbledynamics_fixedstep.py +2929 -0
  4. transitionlistener/check_installation.py +309 -0
  5. transitionlistener/ckm.py +91 -0
  6. transitionlistener/colors.py +288 -0
  7. transitionlistener/config.py +185 -0
  8. transitionlistener/constants.py +64 -0
  9. transitionlistener/counterterms/__init__.py +230 -0
  10. transitionlistener/counterterms/cw.py +298 -0
  11. transitionlistener/counterterms/tensors.py +343 -0
  12. transitionlistener/counterterms/twohdm.py +520 -0
  13. transitionlistener/counterterms/twohdm_solver.py +374 -0
  14. transitionlistener/errors.py +315 -0
  15. transitionlistener/examples/bp_et.yaml +64 -0
  16. transitionlistener/examples/bp_lisa.yaml +70 -0
  17. transitionlistener/examples/bp_muares.yaml +69 -0
  18. transitionlistener/examples/bp_pta.yaml +64 -0
  19. transitionlistener/examples/example_dark_photon.yaml +73 -0
  20. transitionlistener/examples/example_grid.yaml +55 -0
  21. transitionlistener/examples/example_line.yaml +51 -0
  22. transitionlistener/examples/example_point.yaml +73 -0
  23. transitionlistener/examples/example_random_conformal.yaml +42 -0
  24. transitionlistener/examples/example_ultranest.yaml +39 -0
  25. transitionlistener/finiteT.py +468 -0
  26. transitionlistener/generic_potential.py +1521 -0
  27. transitionlistener/gridplots.py +592 -0
  28. transitionlistener/gwfopt.py +604 -0
  29. transitionlistener/helper_functions.py +843 -0
  30. transitionlistener/hydrodynamics.py +647 -0
  31. transitionlistener/interface/__init__.py +95 -0
  32. transitionlistener/interface/check.py +51 -0
  33. transitionlistener/interface/cli.py +211 -0
  34. transitionlistener/interface/config_loader.py +264 -0
  35. transitionlistener/interface/logging_utils.py +30 -0
  36. transitionlistener/interface/output_schema.py +296 -0
  37. transitionlistener/interface/pipeline.py +300 -0
  38. transitionlistener/interface/samplers.py +565 -0
  39. transitionlistener/interface/scans.py +468 -0
  40. transitionlistener/interface/single_point.py +581 -0
  41. transitionlistener/interface/state.py +61 -0
  42. transitionlistener/lineplots.py +597 -0
  43. transitionlistener/logo/TL-logo_large.png +0 -0
  44. transitionlistener/logo/TL-logo_small.png +0 -0
  45. transitionlistener/models/TL_2HDM.py +512 -0
  46. transitionlistener/models/TL_conformal_dark_u1.py +202 -0
  47. transitionlistener/models/TL_dark_U1.py +198 -0
  48. transitionlistener/models/TL_dark_U1_g_parameterization.py +198 -0
  49. transitionlistener/models/TL_dark_flipflop.py +274 -0
  50. transitionlistener/models/templatePotential.py +260 -0
  51. transitionlistener/nucleation.py +412 -0
  52. transitionlistener/observability.py +790 -0
  53. transitionlistener/particles.py +436 -0
  54. transitionlistener/pathDeformation.py +1462 -0
  55. transitionlistener/percolation_adaptive_gridbuilders.py +1385 -0
  56. transitionlistener/percolation_adaptive_rate.py +1049 -0
  57. transitionlistener/percolation_adaptivestepsize.py +1632 -0
  58. transitionlistener/percolation_fixedstepsize.py +618 -0
  59. transitionlistener/phases.py +1499 -0
  60. transitionlistener/plot_settings.py +121 -0
  61. transitionlistener/plots.py +1781 -0
  62. transitionlistener/runtime_options.py +278 -0
  63. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/bandwidths.npy +0 -0
  64. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/density.npy +0 -0
  65. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/freqs.npy +0 -0
  66. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log.txt +16 -0
  67. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rhogrid.npy +0 -0
  68. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rholabels.txt +30 -0
  69. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/pulsar_list.txt +1 -0
  70. transitionlistener/tab_data/HLV-O2-PLI.csv +82 -0
  71. transitionlistener/tab_data/HLV-O2-noise.csv +83 -0
  72. transitionlistener/tab_data/HLVK-design-PLI.csv +61 -0
  73. transitionlistener/tab_data/HLVK-design-noise.csv +83 -0
  74. transitionlistener/tab_data/NG15_PLI_hc.csv +31 -0
  75. transitionlistener/tab_data/NG15_noise_hc.csv +56 -0
  76. transitionlistener/tab_data/PLI-dataset.csv +327 -0
  77. transitionlistener/tab_data/energydensities.dat +1001 -0
  78. transitionlistener/tab_data/finiteT_b.dat.txt +1999 -0
  79. transitionlistener/tab_data/finiteT_f.dat.txt +1000 -0
  80. transitionlistener/tab_data/geff_SM.dat +10009 -0
  81. transitionlistener/tab_data/muAres_f_Hz_PLI_h2OmegaGW.csv +131 -0
  82. transitionlistener/tab_data/muAres_f_Hz_h2Omega_noise.csv +99 -0
  83. transitionlistener/tab_data/noise-dataset.csv +327 -0
  84. transitionlistener/tab_data/numberdensities.dat +1001 -0
  85. transitionlistener/tab_data/pressures.dat +1001 -0
  86. transitionlistener/thermodynamics.py +686 -0
  87. transitionlistener/transitionObservables.py +1199 -0
  88. transitionlistener/transitionObservables_fixedstep.py +976 -0
  89. transitionlistener/transitions.py +1000 -0
  90. transitionlistener/tunneling1D.py +910 -0
  91. transitionlistener-2.0.0.dist-info/METADATA +817 -0
  92. transitionlistener-2.0.0.dist-info/RECORD +95 -0
  93. transitionlistener-2.0.0.dist-info/WHEEL +4 -0
  94. transitionlistener-2.0.0.dist-info/entry_points.txt +2 -0
  95. transitionlistener-2.0.0.dist-info/licenses/LICENSE.txt +674 -0
@@ -0,0 +1,686 @@
1
+ """Module containing routines to calculate thermodynamic quantities
2
+ like the numberdensity, energy density, pressure and entropy density.
3
+
4
+ Part of TransitionListener v2.0
5
+ Documentation: https://tasillo.de/TransitionListener/
6
+
7
+ Authors:
8
+ Jonas Matuszak <jonas.matuszak@kit.edu>
9
+ Carlo Tasillo <carlo.tasillo@ific.uv.es>
10
+ """
11
+
12
+ import numpy as np
13
+
14
+ from scipy import interpolate
15
+ from scipy import integrate
16
+ from scipy import special
17
+ from scipy.signal import savgol_filter
18
+
19
+ from .particles import BaseParticle
20
+ from . import console
21
+
22
+ import os
23
+ myPath = os.path.abspath(os.path.dirname(__file__)) + "/"
24
+
25
+ # Interpolate numberdensities
26
+ def calcNumberdensityIntegrals():
27
+ """Calculate geff for numberdensity"""
28
+ def integf(u,x): return u * np.sqrt(u**2 - x**2) / (np.exp(u) + 1)
29
+ def integb(u,x): return u * np.sqrt(u**2 - x**2) / (np.exp(u) - 1)
30
+
31
+ n = 1000
32
+ x_range = np.logspace(-7, np.log10(7e2), n)
33
+ Ib = np.zeros(n)
34
+ If = np.zeros(n)
35
+ for i, x in enumerate(x_range):
36
+ Ib[i] = integrate.quad(integb, x + 1e-50, np.inf, args=(x,))[0]
37
+ If[i] = integrate.quad(integf, x + 1e-50, np.inf, args=(x,))[0]
38
+
39
+ Ib = Ib / (2*special.zeta(3))
40
+ If = If / (2*special.zeta(3))
41
+ data = np.array([x_range, Ib, If]).T
42
+
43
+ np.savetxt(myPath + "tab_data/numberdensities.dat", data, header="# x, Ib, If")
44
+
45
+
46
+ def calcEnergydensityIntegrals():
47
+ """Tabulate geff for the energydensity"""
48
+ def integf(u, x): return u**2 * np.sqrt(u**2 - x**2) / (np.exp(u) + 1)
49
+ def integb(u, x): return u**2 * np.sqrt(u**2 - x**2) / (np.exp(u) - 1)
50
+
51
+ n = 1000
52
+ x_range = np.logspace(-7, np.log10(7e2), n)
53
+ Ib = np.zeros(n)
54
+ If = np.zeros(n)
55
+ for i, x in enumerate(x_range):
56
+ Ib[i] = integrate.quad(integb, x + 1e-50, np.inf, args=(x,))[0]
57
+ If[i] = integrate.quad(integf, x + 1e-50, np.inf, args=(x,))[0]
58
+
59
+ Ib = Ib * 15/np.pi**4
60
+ If = If * 15/np.pi**4
61
+ data = np.array([x_range, Ib, If]).T
62
+
63
+ np.savetxt(myPath + "tab_data/energydensities.dat", data, header="# x, Ienergy_b, Ienergy_f")
64
+
65
+
66
+ def calcPressureIntegrals():
67
+ """Pre-compute spline approximations of the pressure integrals."""
68
+ def integf(u,x): return np.sqrt(u**2 - x**2)**3 / (np.exp(u) + 1)
69
+ def integb(u,x): return np.sqrt(u**2 - x**2)**3 / (np.exp(u) - 1)
70
+
71
+ n = 1000
72
+ x_range = np.logspace(-7, np.log10(7e2), n)
73
+ Ib = np.zeros(n)
74
+ If = np.zeros(n)
75
+ for i,x in enumerate(x_range):
76
+ Ib[i] = integrate.quad(integb, x + 1e-50, np.inf, args=(x,))[0]
77
+ If[i] = integrate.quad(integf, x + 1e-50, np.inf, args=(x,))[0]
78
+
79
+ Ib = Ib * 15/np.pi**4
80
+ If = If * 15/np.pi**4
81
+ data = np.array([x_range, Ib, If]).T
82
+
83
+ np.savetxt(myPath + "tab_data/pressures.dat", data, header="# x, Ipressure_b, Ipressure_f")
84
+
85
+
86
+ try:
87
+ data_ndensity = np.loadtxt(myPath + "tab_data/numberdensities.dat")
88
+ data_edensity = np.loadtxt(myPath + "tab_data/energydensities.dat")
89
+ data_pressure = np.loadtxt(myPath + "tab_data/pressures.dat")
90
+ except:
91
+ calcNumberdensityIntegrals()
92
+ calcEnergydensityIntegrals()
93
+ calcPressureIntegrals()
94
+ data_ndensity = np.loadtxt(myPath + "tab_data/numberdensities.dat")
95
+ data_edensity = np.loadtxt(myPath + "tab_data/energydensities.dat")
96
+ data_pressure = np.loadtxt(myPath + "tab_data/pressures.dat")
97
+
98
+ data_geffSM = np.genfromtxt(myPath + "tab_data/geff_SM.dat")
99
+ Ie_geffSM = interpolate.CubicSpline(
100
+ np.log10(data_geffSM[:, 0]),
101
+ data_geffSM[:, 1],
102
+ bc_type="clamped"
103
+ )
104
+ # Add filter to smooth for derivative
105
+ Ie_geffSM_smooth = interpolate.CubicSpline(
106
+ np.log10(data_geffSM[:, 0]),
107
+ savgol_filter(data_geffSM[:, 1], 81, 3),
108
+ bc_type="clamped"
109
+ )
110
+
111
+ Is_geffSM = interpolate.CubicSpline(
112
+ np.log10(data_geffSM[:, 0]),
113
+ data_geffSM[:, 3],
114
+ bc_type="clamped"
115
+ )
116
+ Is_geffSM_smooth = interpolate.CubicSpline(
117
+ np.log10(data_geffSM[:, 0]),
118
+ savgol_filter(data_geffSM[:, 3], 81, 3),
119
+ bc_type="clamped"
120
+ )
121
+
122
+
123
+ Inb = interpolate.interp1d(data_ndensity[:,0], data_ndensity[:,1], kind="cubic")
124
+ Inf = interpolate.interp1d(data_ndensity[:,0], data_ndensity[:,2], kind="cubic")
125
+ Ieb = interpolate.interp1d(data_edensity[:,0], data_edensity[:,1], kind="cubic", fill_value=(1, 0), bounds_error=False)
126
+ Ief = interpolate.interp1d(
127
+ data_edensity[:, 0],
128
+ data_edensity[:, 2],
129
+ kind="cubic",
130
+ fill_value=(0.875, 0),
131
+ bounds_error=False,
132
+ )
133
+ Ipb = interpolate.interp1d(data_pressure[:,0], data_pressure[:,1], kind="cubic")
134
+ Ipf = interpolate.interp1d(data_pressure[:,0], data_pressure[:,2], kind="cubic")
135
+
136
+ # ==================================================
137
+ # Fit function for the SM effective DOFs
138
+ # K. Saikawa and S. Shirai (2018), arxiv:1803.01038
139
+ # ==================================================
140
+
141
+ def geff_rho_h(T: float):
142
+ r"""High-temperature fit for the SM energy-density degrees of freedom.
143
+
144
+ The fit approximates :math:`g_{\mathrm{eff},\rho}^{\mathrm{SM}}(T)` defined
145
+ by
146
+
147
+ .. math::
148
+ \rho_{\mathrm{SM}}(T)
149
+ = \frac{\pi^2}{30}\, g_{\mathrm{eff},\rho}^{\mathrm{SM}}(T)\, T^4.
150
+
151
+ It uses the rational approximation of Saikawa and Shirai
152
+ (arXiv:1803.01038) above the QCD crossover.
153
+ """
154
+ a = np.array([1.0, 1.117240, 3.12672e-01,
155
+ -4.68049e-02, -2.65004e-02, -1.19760e-03,
156
+ 1.82812e-04, 1.36436e-04, 8.55051e-05,
157
+ 1.22840e-05, 3.82259e-07, -6.87035e-09])
158
+ b = np.array([1.43382e-02, 1.37559e-02, 2.92108e-03,
159
+ -5.38533e-04, -1.62496e-04, -2.87906e-05,
160
+ -3.84278e-06, 2.78776e-06, 7.40342e-07,
161
+ 1.17210e-07, 3.72499e-09, -6.74107e-11])
162
+
163
+ t = np.log(T)
164
+ x = 1
165
+ temp1 = 0
166
+ temp2 = 0
167
+ for i in range(12):
168
+ temp1 += a[i] * x
169
+ temp2 += b[i] * x
170
+ x *= t
171
+ return temp1/temp2
172
+
173
+ def ratio(T: float):
174
+ r"""Return the fitted ratio :math:`g_{\mathrm{eff},\rho}/g_{\mathrm{eff},s}`.
175
+
176
+ This auxiliary fit is combined with :func:`geff_rho_h` to reconstruct the
177
+ entropy degrees of freedom according to
178
+
179
+ .. math::
180
+ g_{\mathrm{eff},s}(T)
181
+ = \frac{g_{\mathrm{eff},\rho}(T)}
182
+ {g_{\mathrm{eff},\rho}(T)/g_{\mathrm{eff},s}(T)}.
183
+
184
+ This function implements the ratio in the denominator.
185
+ """
186
+ a = np.array([1., 6.07869e-01, -1.54485e-01,
187
+ -2.24034e-01, -2.82147e-02, 2.90620e-02,
188
+ 6.86778e-03, -1.05e-03, -1.69104e-04,
189
+ 1.06301e-05, 1.69528e-06, -9.33311e-08])
190
+
191
+ b = np.array([7.07388E+01, 9.18011E+01, 3.31892E+01,
192
+ -1.39779E+00, -1.52558E+00, -1.97857e-02,
193
+ -1.60146e-01, 8.22615e-05, 2.02651e-02,
194
+ -1.82134e-05, 7.83943e-05, 7.13518e-05])
195
+
196
+ t = np.log(T)
197
+ x = 1
198
+ temp1 = 0
199
+ temp2 = 0
200
+ for i in range(12):
201
+ temp1 += a[i] * x
202
+ temp2 += b[i] * x
203
+ x *= t
204
+
205
+ return temp1 / temp2 + 1
206
+
207
+
208
+ def geff_s_h(T: float):
209
+ r"""High-temperature fit for the SM entropy degrees of freedom.
210
+
211
+ The quantity is defined by
212
+
213
+ .. math::
214
+ s_{\mathrm{SM}}(T)
215
+ = \frac{2\pi^2}{45}\, g_{\mathrm{eff},s}^{\mathrm{SM}}(T)\, T^3.
216
+ """
217
+ return 1 / ratio(T) * geff_rho_h(T)
218
+
219
+
220
+ def fr(x: float):
221
+ """Low-temperature fermionic energy-density kernel used in the tabulated SM fit."""
222
+ return np.exp(-1.04855 * x) * \
223
+ (1 + 1.03757 * x + 0.508630 * x * x + 0.0893988 * x * x * x)
224
+
225
+ def br(x: float):
226
+ """Low-temperature bosonic energy-density kernel used in the tabulated SM fit."""
227
+ return np.exp(-1.03149 * x) * \
228
+ (1 + 1.03317 * x + 0.398264 * x * x + 0.0648056 * x * x * x)
229
+
230
+ def fs(x: float):
231
+ """Low-temperature fermionic entropy-density kernel used in the tabulated SM fit."""
232
+ return np.exp(-1.04190 * x) * \
233
+ (1. + 1.03400 * x + 0.456426 * x * x + 0.0595248 * x * x * x)
234
+
235
+
236
+ def bs(x: float):
237
+ """Low-temperature bosonic entropy-density kernel used in the tabulated SM fit."""
238
+ return np.exp(-1.03365 * x) * \
239
+ (1. + 1.03397 * x + 0.342548 * x * x + 0.0506182 * x * x * x)
240
+
241
+ def Sfit(x: float):
242
+ """Auxiliary suppression factor appearing in the low-temperature SM fits."""
243
+ return 1. + 7. / 4. * np.exp(-1.0419 * x) * \
244
+ (1. + 1.034 * x + 0.456426 * x * x + 0.0595249 * x * x * x)
245
+
246
+ def geff_rho_l(T, p1 = 1, p2 = 1):
247
+ r"""Low-temperature fit for :math:`g_{\mathrm{eff},\rho}^{\mathrm{SM}}`.
248
+
249
+ Below the QCD crossover the fit is written as a sum of hadronic and leptonic
250
+ threshold functions and still satisfies
251
+
252
+ .. math::
253
+ \rho_{\mathrm{SM}}(T)
254
+ = \frac{\pi^2}{30}\, g_{\mathrm{eff},\rho}^{\mathrm{SM}}(T)\, T^4.
255
+ """
256
+ me = 511e-6; mmu = 0.1056; mpi0 = 0.135; mpip = 0.140; m1 = 0.5
257
+ m2 = 0.77; m3 = 1.2; m4 = 2.0
258
+ return 2.030 + 1.353 * np.power(Sfit(me / T), 4. / 3.) + 3.495 * fr(me / T) + \
259
+ 3.446 * fr(mmu / T) + 1.05 * br(mpi0 / T) + 2.08 * br(mpip / T) + \
260
+ 4.165 * br(m1 / T) + 30.55 * br(m2 / T) + 89.4 * br(p2 * m3 / T) + \
261
+ 8209 * br(p2 * m4 / T)
262
+
263
+ def geff_s_l(T: float, p1=1, p2=1):
264
+ r"""Low-temperature fit for :math:`g_{\mathrm{eff},s}^{\mathrm{SM}}`.
265
+
266
+ The fit is defined through
267
+
268
+ .. math::
269
+ s_{\mathrm{SM}}(T)
270
+ = \frac{2\pi^2}{45}\, g_{\mathrm{eff},s}^{\mathrm{SM}}(T)\, T^3.
271
+ """
272
+ me = 511e-6; mmu = 0.1056; mpi0 = 0.135; mpip = 0.140; m1 = 0.5
273
+ m2 = 0.77; m3 = 1.2; m4 = 2.0
274
+ return 2.008 + 1.923 * np.power(Sfit(me / T), 1.) + 3.442 * fs(me / T) + \
275
+ 3.468 * fs(mmu / T) + 1.034 * bs(mpi0 / T) + 2.068 * bs(mpip / T) + \
276
+ 4.160 * bs(m1 / T) + 30.55 * bs(m2 / T) + 90 * bs(p2 * m3 / T) + \
277
+ 6209 * br(p2 * m4 / T)
278
+
279
+
280
+ def _e_geffSM_fit(T: float, CF: float):
281
+ """Dispatch between the low- and high-temperature SM energy-density fits."""
282
+ if (T < 0.12):
283
+ return geff_rho_l(T)
284
+ return geff_rho_h(T)
285
+
286
+ e_geffSM_fit = np.vectorize(_e_geffSM_fit)
287
+
288
+ def _s_geffSM_fit(T: float, CF: float):
289
+ """Dispatch between the low- and high-temperature SM entropy-density fits."""
290
+ if (T < 0.12):
291
+ return geff_s_l(T)
292
+ return geff_s_h(T)
293
+
294
+ s_geffSM_fit = np.vectorize(_s_geffSM_fit)
295
+
296
+ def p_geffSM_fit(TSM: float | np.ndarray, CF: float) -> float | np.ndarray:
297
+ """
298
+ Effective pressure degress of freedom for the SM.
299
+
300
+ Parameters
301
+ ----------
302
+ TSM : float or np.ndarray
303
+ Temperature in internal energy units.
304
+ CF : float
305
+ Conversion factor to put everything in GeV.
306
+ """
307
+ TSM_arr = np.asarray(TSM, dtype=float)
308
+ TSM_GeV = TSM_arr * CF
309
+ if _sm_mass_cap_GeV is not None:
310
+ TSM_GeV = np.minimum(TSM_GeV, _sm_mass_cap_GeV)
311
+ s_val = np.asarray(s_geffSM_fit(TSM_GeV), dtype=float)
312
+ e_val = np.asarray(e_geffSM_fit(TSM_GeV), dtype=float)
313
+ p_base = 4 * s_val - 3 * e_val
314
+
315
+ return float(p_base) if np.asarray(p_base).shape == () else p_base
316
+
317
+
318
+ _sm_mass_cap_GeV: float | None = None
319
+
320
+
321
+ def set_sm_temperature_cap(
322
+ CF: float,
323
+ mSq_bosons: tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None = None,
324
+ boson_is_SM: np.ndarray | None = None,
325
+ mSq_fermions: tuple[np.ndarray, np.ndarray, np.ndarray] | None = None,
326
+ fermion_is_SM: np.ndarray | None = None,
327
+ verbose: bool = False,
328
+ ) -> float | None:
329
+ """Initialise the SM temperature cap from the spectrum at the true minimum.
330
+
331
+ Parameters
332
+ ----------
333
+ CF : float
334
+ Conversion factor to GeV.
335
+ mSq_bosons, mSq_fermions : tuple, optional
336
+ Spectra evaluated at the true vacuum.
337
+ verbose : bool, optional
338
+ Print a warning containing the lightest SM mass when saturation is applied.
339
+ """
340
+ global _sm_mass_cap_GeV
341
+
342
+ masses = []
343
+ selected_boson_m2 = np.array([], dtype=float)
344
+ selected_fermion_m2 = np.array([], dtype=float)
345
+
346
+ # Numerical massless floor:
347
+ # treat masses below one ULP of the characteristic SM m^2 scale as zero.
348
+ # The scale is computed in GeV^2 and mapped back to internal units with CF.
349
+ eps = np.finfo(float).eps
350
+
351
+ # Add only physical, non-BSM boson masses to the list
352
+ if mSq_bosons is not None:
353
+ m2b, _, _, physical = mSq_bosons
354
+ mask = (np.asarray(boson_is_SM, dtype=bool)) & np.asarray(physical, dtype=bool)
355
+ if np.any(mask):
356
+ selected = np.asarray(m2b, dtype=float)[..., mask]
357
+ selected_boson_m2 = np.abs(selected).ravel()
358
+
359
+ # Add only non-BSM fermion masses to the list
360
+ if mSq_fermions is not None:
361
+ m2f, _ = mSq_fermions
362
+ mask = np.asarray(fermion_is_SM, dtype=bool)
363
+ if np.any(mask):
364
+ selected = np.asarray(m2f, dtype=float)[..., mask]
365
+ selected_fermion_m2 = np.abs(selected).ravel()
366
+
367
+ selected_boson_m2 = selected_boson_m2[np.isfinite(selected_boson_m2)]
368
+ selected_fermion_m2 = selected_fermion_m2[np.isfinite(selected_fermion_m2)]
369
+
370
+ if selected_boson_m2.size:
371
+ m2_scale_b_internal = float(np.max(selected_boson_m2))
372
+ m2_scale_b_GeV2 = m2_scale_b_internal * CF * CF
373
+ m2_tol_b_internal = (eps * m2_scale_b_GeV2) / (CF * CF)
374
+ else:
375
+ m2_tol_b_internal = 0.0
376
+
377
+ if selected_fermion_m2.size:
378
+ m2_scale_f_internal = float(np.max(selected_fermion_m2))
379
+ m2_scale_f_GeV2 = m2_scale_f_internal * CF * CF
380
+ m2_tol_f_internal = (eps * m2_scale_f_GeV2) / (CF * CF)
381
+ else:
382
+ m2_tol_f_internal = 0.0
383
+
384
+ # Collect non-massless states using the numerical floor.
385
+ if mSq_bosons is not None:
386
+ m2b, _, _, physical = mSq_bosons
387
+ mask = (np.asarray(boson_is_SM, dtype=bool)) & np.asarray(physical, dtype=bool)
388
+ if np.any(mask):
389
+ selected = np.asarray(m2b, dtype=float)[..., mask]
390
+ masses.extend(np.sqrt(selected[selected > m2_tol_b_internal]).ravel())
391
+
392
+ if mSq_fermions is not None:
393
+ m2f, _ = mSq_fermions
394
+ mask = np.asarray(fermion_is_SM, dtype=bool)
395
+ if np.any(mask):
396
+ selected = np.asarray(m2f, dtype=float)[..., mask]
397
+ masses.extend(np.sqrt(selected[selected > m2_tol_f_internal]).ravel())
398
+
399
+ if masses:
400
+ lightest = float(np.min(masses))
401
+ previous_cap = _sm_mass_cap_GeV
402
+ _sm_mass_cap_GeV = 0.5 * lightest * CF
403
+ if verbose and previous_cap is None:
404
+ msg = (f"The lightest (non-massless) SM particle in the model "
405
+ f"file has a mass of {lightest * CF:.3g} GeV. The tabulated SM "
406
+ f"effective degrees of freedom will hence be capped at "
407
+ f"{_sm_mass_cap_GeV:.3g} GeV. The model will compute the "
408
+ f"effective degrees of freedom dynamically beyond that point.")
409
+ console.print(
410
+ f"[yellow]Warning: {msg}[/yellow]"
411
+ )
412
+ else:
413
+ _sm_mass_cap_GeV = None
414
+
415
+ return _sm_mass_cap_GeV
416
+
417
+
418
+ def e_geffSM(
419
+ TSM: float | np.ndarray,
420
+ CF: float,
421
+ mode: str = "smooth"
422
+ ) -> float | np.ndarray:
423
+ """
424
+ Effective energy degrees of freedom of the SM
425
+ at a given temperature.
426
+
427
+ Parameters
428
+ ----------
429
+ TSM : float or np.ndarray
430
+ Temperature in internal energy units.
431
+ CF : float
432
+ Conversion factor to put everything in GeV.
433
+ mode : str, optional
434
+ "smooth", "fit" or "data"
435
+ """
436
+ TSM_arr = np.asarray(TSM, dtype=float)
437
+ capped_T = TSM_arr * CF
438
+ if _sm_mass_cap_GeV is not None:
439
+ capped_T = np.minimum(capped_T, _sm_mass_cap_GeV)
440
+ if mode == "smooth":
441
+ result = np.asarray(Ie_geffSM_smooth(np.log10(capped_T)), dtype=float)
442
+ elif mode == "data":
443
+ result = np.asarray(Ie_geffSM(np.log10(capped_T)), dtype=float)
444
+ elif mode == "fit":
445
+ result = e_geffSM_fit(TSM, CF)
446
+
447
+ return float(result) if result.shape == () else result
448
+
449
+
450
+ def s_geffSM(TSM: float | np.ndarray, CF: float, mode: str = "smooth"
451
+ ) -> float | np.ndarray:
452
+ """
453
+ Effective entropy degrees of freedom of the SM
454
+ at a given temperature.
455
+
456
+ Parameters
457
+ ----------
458
+ TSM : float or np.ndarray
459
+ Temperature in internal energy units.
460
+ CF : float
461
+ Conversion factor to put everything in GeV.
462
+ mode : str, optional
463
+ "fit", "smooth" or "data".
464
+ """
465
+ TSM_arr = np.asarray(TSM, dtype=float)
466
+ capped_T = TSM_arr * CF
467
+ if _sm_mass_cap_GeV is not None:
468
+ capped_T = np.minimum(capped_T, _sm_mass_cap_GeV)
469
+ if mode == "smooth":
470
+ result = np.asarray(Is_geffSM_smooth(np.log10(capped_T)), dtype=float)
471
+ elif mode == "data":
472
+ result = np.asarray(Is_geffSM(np.log10(capped_T)), dtype=float)
473
+ elif mode == "fit":
474
+ result = s_geffSM_fit(TSM, CF)
475
+
476
+ return float(result) if result.shape == () else result
477
+
478
+
479
+ def p_geffSM(TSM: float | np.ndarray, CF: float, mode: str = "smooth"
480
+ ) -> float | np.ndarray:
481
+ """
482
+ Effective pressure degress of freedom for the SM.
483
+
484
+ Parameters
485
+ ----------
486
+ TSM : float or np.ndarray
487
+ Temperature in internal energy units.
488
+ CF : float
489
+ Conversion factor to put everything in GeV.
490
+ mode : str, optional
491
+ Either "fit", "smooth" or "data".
492
+ """
493
+ TSM_arr = np.asarray(TSM, dtype=float)
494
+ TSM_GeV = TSM_arr * CF
495
+ if _sm_mass_cap_GeV is not None:
496
+ TSM_GeV = np.minimum(TSM_GeV, _sm_mass_cap_GeV)
497
+ if mode == "smooth":
498
+ s_val = np.asarray(Is_geffSM_smooth(np.log10(TSM_GeV)), dtype=float)
499
+ e_val = np.asarray(Ie_geffSM_smooth(np.log10(TSM_GeV)), dtype=float)
500
+ elif mode == "data":
501
+ s_val = np.asarray(Is_geffSM(np.log10(TSM_GeV)), dtype=float)
502
+ e_val = np.asarray(Ie_geffSM(np.log10(TSM_GeV)), dtype=float)
503
+ elif mode == "fit":
504
+ s_val = s_geffSM_fit(TSM, CF)
505
+ e_val = e_geffSM_fit(TSM, CF)
506
+ p_base = 4 * s_val - 3 * e_val
507
+
508
+ return float(p_base) if np.asarray(p_base).shape == () else p_base
509
+
510
+ def nDensity(m, T, g, ptype):
511
+ """Numberdensity from tabulated values."""
512
+ pref = special.zeta(3)/np.pi**2 * g * T**3
513
+ if T == 0.0:
514
+ return 0.0
515
+ x = m/T
516
+ res = 0.0
517
+ if ptype == "b":
518
+ if x < Inb.x[0]:
519
+ res = pref
520
+ elif x < Inb.x[-1]:
521
+ res = pref * Inb(x)
522
+ elif ptype == "f":
523
+ if x < Inb.x[0]:
524
+ res = (3/4) * pref
525
+ elif x < Inb.x[-1]:
526
+ res = pref * Inf(x)
527
+ return res
528
+
529
+
530
+ def e_geff(particles: BaseParticle | list[BaseParticle], X: np.ndarray, T: float | np.ndarray):
531
+ """
532
+ Calculate the particles effective degrees of freedom.
533
+ """
534
+ T += 1e-100
535
+
536
+ if not isinstance(particles, list):
537
+ particles = [particles]
538
+
539
+ geff = np.zeros_like(X)
540
+
541
+ for p in particles:
542
+ masses = p.evaluate_prefactor(X)
543
+ if masses is None:
544
+ raise AttributeError(
545
+ "Thermodynamic routines require particles with thermal mass prefactors"
546
+ )
547
+ m = np.sqrt(np.asarray(masses))
548
+ x = m / T
549
+ res = np.zeros_like(X)
550
+ if p.statistic == "fermion":
551
+ res = p.dof * np.where(x < Ief.x[0], 7 / 8, res)
552
+ res = p.dof * np.where((x >= Ief.x[0]) & (x < Ief.x[-1]), Ief(x), res)
553
+ elif p.statistic == "boson":
554
+ res = p.dof * np.where(x < Ieb.x[0], 1.0, res)
555
+ res = p.dof * np.where((x >= Ieb.x[0]) & (x < Ieb.x[-1]), Ieb(x), res)
556
+ geff += res
557
+ return geff
558
+
559
+
560
+ def p_geff(particles: BaseParticle | list[BaseParticle], X: np.ndarray, T: float | np.ndarray):
561
+ """
562
+ Calculate the particles pressure effective degrees of freedom.
563
+ """
564
+ T += 1e-100
565
+
566
+ if not isinstance(particles, list):
567
+ particles = [particles]
568
+
569
+ geff = np.zeros_like(X)
570
+
571
+ for p in particles:
572
+ masses = p.evaluate_prefactor(X)
573
+ if masses is None:
574
+ raise AttributeError(
575
+ "Thermodynamic routines require particles with thermal mass prefactors"
576
+ )
577
+ m = np.sqrt(np.asarray(masses))
578
+ x = m / T
579
+ res = np.zeros_like(X)
580
+ if p.statistic == "fermion":
581
+ res = p.dof * np.where(x < Ipf.x[0], 7 / 8, res)
582
+ res = p.dof * np.where((x >= Ipf.x[0]) & (x < Ipf.x[-1]), Ipf(x), res)
583
+ elif p.statistic == "boson":
584
+ res = p.dof * np.where(x < Ipb.x[0], 1.0, res)
585
+ res = p.dof * np.where((x >= Ipb.x[0]) & (x < Ipb.x[-1]), Ipb(x), res)
586
+ geff += res
587
+ return geff
588
+
589
+
590
+ def e_geff(m, T, g, ptype):
591
+ """Energy degrees of freedom, supports scalar or vector inputs for m and T."""
592
+ m = np.asarray(m) # Ensure m is a numpy array
593
+ T = np.asarray(T) # Ensure T is a numpy array
594
+ T = T + 1e-100
595
+ x = np.where(T != 0, m / T, np.inf) # Avoid division by zero
596
+ res = np.zeros_like(x)
597
+
598
+ if ptype == "b":
599
+ res = np.where(x < Ieb.x[0], 1.0, res)
600
+ res = np.where((x >= Ieb.x[0]) & (x < Ieb.x[-1]), Ieb(x), res)
601
+ elif ptype == "f":
602
+ res = np.where(x < Ief.x[0], 7 / 8, res)
603
+ res = np.where((x >= Ief.x[0]) & (x < Ief.x[-1]), Ief(x), res)
604
+ return res * g
605
+
606
+
607
+ def p_geff(m, T, g, ptype):
608
+ """Pressure degrees of freedom, supports scalar or vector inputs for m and T."""
609
+ if T == 0.0:
610
+ return 0.0
611
+ x = m/T
612
+ res = 0.0
613
+ if ptype == "b":
614
+ if x < Ipb.x[0]:
615
+ res = 1.0
616
+ elif x < Ipb.x[-1]:
617
+ res = Ipb(x)
618
+ elif ptype == "f":
619
+ if x < Ipf.x[0]:
620
+ res = (7/8)
621
+ elif x < Ipf.x[-1]:
622
+ res = Ipf(x)
623
+ return res * g
624
+
625
+ def s_geff(m, T, g, ptype):
626
+ """Entropy degrees of freedom, supports scalar or vector inputs for m and T."""
627
+ return (3*e_geff(m, T, g, ptype) + p_geff(m, T, g, ptype))/4
628
+
629
+
630
+ def s_geffDS(mSq_bosons, mSq_fermions, T):
631
+ """Entropy degrees of freedom of the dark sector"""
632
+ m2b, gb, c, physical = mSq_bosons
633
+ m2f, gf = mSq_fermions
634
+
635
+ geff = 0.0
636
+ for i in range(len(m2b)):
637
+ if physical[i]:
638
+ if m2b[i] > 0:
639
+ geff += s_geff(np.sqrt(m2b[i]), T, gb[i], 'b')
640
+ else:
641
+ # In general, tachyonic modes should not be included
642
+ # in the effective degrees of freedom, but we include them
643
+ # here, assuming that in the plasma they
644
+ # are not tachyonic anymore. One example is the SM Higgs
645
+ # field, which is tachyonic in the symmetric phase,
646
+ # but still contributes to the effective degrees of freedom
647
+ # with 4 degrees of freedom before the phase transition.
648
+ # Truly tachyonic modes, which also
649
+ # don't receive large enough plasma masses in order to have
650
+ # m^2 > 0, should be excluded in the sum.
651
+ # This should be checked in the model / be implemented through the
652
+ # physical flag.
653
+ geff += gb[i]
654
+ for i in range(len(m2f)):
655
+ geff += s_geff(np.sqrt(m2f[i]), T, gf[i], 'f')
656
+ return geff
657
+
658
+ def e_geffDS(mSq_bosons, mSq_fermions, T):
659
+ """Energy degrees of freedom of the dark sector, supports vectorized T."""
660
+ m2b, gb, c, physical = mSq_bosons
661
+ m2f, gf = mSq_fermions
662
+
663
+ T = np.asarray(T) # Ensure T is a numpy array
664
+ geff = np.zeros_like(T, dtype=float)
665
+
666
+ for i in range(len(m2b)):
667
+ if physical[i]:
668
+ if m2b[i] > 0:
669
+ geff += e_geff(np.sqrt(m2b[i]), T, gb[i], 'b')
670
+ else:
671
+ # In general, tachyonic modes should not be included
672
+ # in the effective degrees of freedom, but we include them
673
+ # here, assuming that in the plasma they
674
+ # are not tachyonic anymore. One example is the SM Higgs
675
+ # field, which is tachyonic in the symmetric phase,
676
+ # but still contributes to the effective degrees of freedom
677
+ # with 4 degrees of freedom before the phase transition.
678
+ # Truly tachyonic modes, which also
679
+ # don't receive large enough plasma masses in order to have
680
+ # m^2 > 0, should be excluded in the sum.
681
+ # This should be checked in the model / be implemented through the
682
+ # physical flag.
683
+ geff += gb[i] # modes with negative mass are inlcuded
684
+ for i in range(len(m2f)):
685
+ geff += e_geff(np.sqrt(m2f[i]), T, gf[i], 'f')
686
+ return geff