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,2929 @@
1
+ """
2
+ This module is used to calculate the bubble dynamics in the dark sector.
3
+ It contains functions to calculate the energy density, effective degrees of freedom,
4
+ the Hubble parameter, the bubble nucleation rate, and from this the nucleation
5
+ and percolation temperature.
6
+
7
+ Part of TransitionListener v2.0
8
+ Documentation: https://tasillo.de/TransitionListener/
9
+
10
+ Authors:
11
+ Jonas Matuszak <jonas.matuszak@kit.edu>
12
+ Carlo Tasillo <carlo.tasillo@ific.uv.es>
13
+ """
14
+
15
+ import numpy as np
16
+ from dataclasses import dataclass
17
+ from scipy import optimize
18
+ from scipy import interpolate
19
+ from scipy import integrate
20
+
21
+ from transitionlistener import thermodynamics as td
22
+ from transitionlistener import constants as cn
23
+ from transitionlistener.helper_functions import derivative
24
+ from transitionlistener import errors
25
+ from transitionlistener.pathDeformation import bounceAction
26
+ from transitionlistener.finiteT import Jb_spline as Jb
27
+ from transitionlistener.finiteT import Jf_spline as Jf
28
+
29
+
30
+ def g_eff_DS(T_DS: float, pot, phase) -> float:
31
+ """Calculate the effective energy degrees of freedom in the dark sector.
32
+
33
+ Parameters
34
+ ----------
35
+ T : float
36
+ Temperature at which to evaluate g_eff
37
+ pot : generic_potential
38
+ The effetive potential object
39
+ phase : PhaseInfo
40
+ The phase of the system, either symmetric or broken.
41
+
42
+ Returns
43
+ ----------
44
+ float
45
+ Effective degrees of freedom in the dark sector."""
46
+ try:
47
+ vevT = phase.valAt(T_DS)
48
+ except BaseException:
49
+ print("Warning: TBRO is too low for interpolation of vev, using T = 0 value")
50
+ # Temperature is to low for interpolation of vev, use T = 0 value
51
+ vevT = pot.X0
52
+ bosons = pot.boson_massSq(vevT, 0)*(~pot.mass_spectrum.is_SM_bosons)
53
+ fermions = pot.fermion_massSq(vevT)*(~pot.mass_spectrum.is_SM_fermions)
54
+ geff = td.e_geffDS(bosons, fermions, T_DS)
55
+ return geff
56
+
57
+
58
+ def h_eff_DS(T_DS: float, pot, phase) -> float:
59
+ """Calculate the effective entropy degrees of freedom in the dark sector.
60
+
61
+ Parameters
62
+ ----------
63
+ T : float
64
+ Temperature at which to evaluate g_eff
65
+ pot : generic_potential
66
+ The effetive potential object
67
+ phase : PhaseInfo
68
+ The phase of the system, either symmetric or broken.
69
+
70
+ Returns
71
+ ----------
72
+ geff : float
73
+ Effective degrees of freedom in the dark sector."""
74
+ try:
75
+ vevT = phase.valAt(T_DS)
76
+ except BaseException:
77
+ print("Warning: TBRO is too low for interpolation of vev, using T = 0 value")
78
+ # Temperature is to low for interpolation of vev, use T = 0 value
79
+ vevT = pot.X0
80
+
81
+ # Set SM masses to zero:
82
+ bosons = pot.boson_massSq(vevT, 0)*(~pot.mass_spectrum.is_SM_bosons)
83
+ fermions = pot.fermion_massSq(vevT)*(~pot.mass_spectrum.is_SM_fermions)
84
+ geff = td.s_geffDS(bosons, fermions, T_DS)
85
+ return geff
86
+
87
+
88
+ def energyDensity(pot, phase, T: float | np.ndarray, include_decoupled=True) -> float | np.ndarray:
89
+ r"""This function calls the implementation in the effective potential.
90
+
91
+ Parameters
92
+ ----------
93
+ pot : generic_potential
94
+ Effective potential
95
+ phase : PhaseInfo
96
+ The phase information
97
+ T : float|np.ndarray
98
+ The temperature at which to compute the energy density
99
+ include_decoupled : bool, optional
100
+ If true, also account for the energy density in a decoupled sector
101
+
102
+ Returns
103
+ -------
104
+ float|np.ndarray :
105
+ The energy density at `T`."""
106
+ X = phase.valAt(T)
107
+ return pot.energyDensity(X, T, include_decoupled=include_decoupled)
108
+
109
+
110
+ def Gamma(T: float | np.ndarray, S: float | np.ndarray) -> np.ndarray:
111
+ """Calculate the bubble nucleation rate.
112
+
113
+ Parameters
114
+ ----------
115
+ T : float | np.ndarray
116
+ Symmetric phase temperature
117
+ S : float | np.ndarray
118
+ The action at temperature T
119
+
120
+ Returns
121
+ ----------
122
+ Gamma : np.ndarray
123
+ The bubble nucleation rate."""
124
+ S = np.atleast_1d(S)
125
+ T = np.atleast_1d(T)
126
+ result = np.zeros_like(T, dtype=float)
127
+
128
+ mask_zero = S == 0
129
+ mask_inf = np.isinf(S)
130
+ mask_valid = ~(mask_zero | mask_inf)
131
+
132
+ result[mask_zero] = np.inf
133
+ result[mask_inf] = np.nan
134
+ # Ignore RuntimeWarnings in sqrt and exp (e.g., for invalid or overflow values)
135
+ with np.errstate(invalid="ignore", over="ignore"):
136
+ result[mask_valid] = (
137
+ T[mask_valid] ** 4
138
+ * np.sqrt(S[mask_valid] / (2 * np.pi * T[mask_valid])) ** 3
139
+ * np.exp(-S[mask_valid] / T[mask_valid])
140
+ )
141
+
142
+ return result
143
+
144
+
145
+ def logGamma(T: float | np.ndarray, S: float | np.ndarray) -> np.ndarray:
146
+ """Calculate the log10 of the bubble nucleation rate.
147
+
148
+ Parameters
149
+ ----------
150
+ T : float | np.ndarray
151
+ Symmetric phase temperature
152
+ S : float | np.ndarray
153
+ The action at temperature T
154
+
155
+ Returns
156
+ ----------
157
+ Gamma : np.ndarray
158
+ The bubble nucleation rate."""
159
+ S = np.atleast_1d(S)
160
+ T = np.atleast_1d(T)
161
+ result = np.zeros_like(T, dtype=float)
162
+
163
+ mask_zero = S == 0
164
+ mask_inf = np.isinf(S)
165
+ mask_valid = ~(mask_zero | mask_inf)
166
+
167
+ result[mask_zero] = np.inf
168
+ result[mask_inf] = np.nan
169
+ # Ignore RuntimeWarnings in sqrt and exp (e.g., for invalid or overflow values)
170
+ with np.errstate(invalid="ignore", over="ignore"):
171
+ result[mask_valid] = (
172
+ 4 * np.log(T[mask_valid])
173
+ + 3 / 2 * np.log(S[mask_valid] / (2 * np.pi * T[mask_valid]))
174
+ - S[mask_valid] / T[mask_valid]
175
+ )
176
+
177
+ return result
178
+
179
+
180
+ def HubbleParameter(rho: float | np.ndarray, CF: float) -> float:
181
+ """Hubble parameter.
182
+
183
+ Parameters
184
+ ----------
185
+ rho : float
186
+ The energy density of the unverse.
187
+ CF : float
188
+ Conversion factor to convert internal units to GeV.
189
+ Returns
190
+ -------
191
+ float :
192
+ The Hubble parameter."""
193
+ return np.sqrt(8 * np.pi / 3 * rho) / (cn.Mpl_GeV / CF)
194
+
195
+
196
+ def scalefactorRatio(pot, T1, T2, X) -> float:
197
+ """Compute the scalefactor ratio a(T1)/a(T2)
198
+
199
+ Parameters
200
+ ----------
201
+
202
+ Returns
203
+ -------
204
+
205
+ """
206
+ if T1 == T2:
207
+ return 1
208
+ n = 100
209
+ Trange = np.linspace(T1, T2, n)
210
+ dVdT = np.array([pot.dVdT(X, T, dT=0.1) for T in Trange])
211
+ d2VdT2 = np.array([pot.d2VdT2(X, T) for T in Trange])
212
+
213
+ intgr = integrate.trapezoid(d2VdT2/(3*dVdT), x=Trange)
214
+ return np.exp(intgr)
215
+
216
+ def calcAction(pot, T: float, start_phase, end_phase, outdict: dict, verbose: bool = False,
217
+ phitol: float = 1e-6) -> float:
218
+ """Calculate the action at temperature `T`
219
+
220
+ Parameters
221
+ ----------
222
+ pot : generic_potential
223
+ The effective potential object
224
+ T : float
225
+ The temperature
226
+ start_phase : PhaseInfo
227
+ The information about the high temperature phase
228
+ end_phase : PhaseInfo
229
+ The information about the low temperature phase
230
+ outdict : dict
231
+ The dictionary storing the action evaluations with the key `T`.
232
+ verbose : bool, optional
233
+ Set the output level
234
+ phitol : float, optional
235
+ Set the accuracy of the minimisation of the potential minima.
236
+
237
+ Returns
238
+ -------
239
+ float :
240
+ The action at temperature `T`."""
241
+
242
+ x0 = findLocalMinimum(start_phase.valAt(T), T, pot.gradV, pot.d2V)
243
+ x1 = findLocalMinimum(end_phase.valAt(T), T, pot.gradV, pot.d2V)
244
+ tdict = dict(low_vev=x1, high_vev=x0)
245
+
246
+ if T in outdict:
247
+ return outdict[T]["action"]
248
+
249
+ outdict = bounceAction(
250
+ T, pot.Vtot, pot.gradV, outdict, tdict, verbose, pot.conversionFactor,
251
+ **pot.config.tracingConf.tunneling_params)
252
+ return outdict[T]["action"]
253
+
254
+
255
+ def approxPercIntegral(
256
+ Tperc: float,
257
+ logG: float,
258
+ Tnuc: float,
259
+ beta: float,
260
+ alphasq: float,
261
+ pot,
262
+ phase,
263
+ verbose: bool = False,
264
+ ) -> float:
265
+ r"""Calculate the percolation integral up to the gaussian
266
+ approximation.
267
+
268
+ .. math::
269
+ I = \int_{-\infty}^{t_p} d t (t_p - t) \Gamma_p
270
+ \exp(\beta (t - t_p) - a^2/2(t - t_p)^2)
271
+
272
+ Parameters
273
+ ----------
274
+ Tperc : float,
275
+ The percolation temperature
276
+ logG : float
277
+ :math:`\log(\Gamma_p)`, the log of the nucleation rate
278
+ Tnuc : float
279
+ The nucleation temperature
280
+ beta : float
281
+ First derivative of the action w.r.t. time: :math:`d/dt (S_3/T)`
282
+ alphasq : float
283
+ Second derivative of the action w.r.t. time :math:`d^2/dt^2 (S_3/T)`
284
+ pot : "generic_potential"
285
+ The effective potential
286
+ phase: PhaseInfo
287
+ The current phase
288
+ verbose: bool, optional
289
+ Set the output level
290
+ Returns
291
+ -------
292
+ float:
293
+ The percolation interal.
294
+ """
295
+ assert Tperc <= Tnuc
296
+
297
+ # T to temperature:
298
+ Trange = np.linspace(Tperc, Tnuc, num=200)
299
+ rho_range = np.zeros_like(Trange)
300
+ for i, T in enumerate(Trange):
301
+ rho_range[i] = energyDensity(pot, phase, T)
302
+ integr = 1 / (Trange * HubbleParameter(rho_range, pot.conversionFactor))
303
+ trange = integrate.cumulative_trapezoid(integr, x=Trange, initial=0)
304
+ trange = trange - trange[-1] # set tperc = 0
305
+
306
+ def fG(t):
307
+ res = (-t) ** 3 * np.exp(logG + beta * t - 1 / 2 * alphasq * t**2)
308
+ return res
309
+
310
+ Iperc = integrate.trapezoid(fG(trange), x=trange)
311
+ return Iperc
312
+
313
+
314
+ def percIntegral(T: np.ndarray, H: np.ndarray, S: np.ndarray, vw=1.0) -> float:
315
+ """Perform the percolation integral beteen Tstart and Tend.
316
+
317
+ Parameters
318
+ ----------
319
+ T : np.ndarray
320
+ Temperature of the dark sector symmetric phase. It must start with the
321
+ nucleation temperature and go down to the percolation temp.
322
+ H : np.ndarray
323
+ Hubble rate evaluated at T
324
+ S : np.ndarray
325
+ S3 Euclidian bounce action evaluated at T
326
+ vw : float
327
+
328
+ Returns
329
+ -------
330
+ float
331
+ The integral evaluated at the last temperature ``T[-1]``.
332
+ """
333
+ # See eq. (4.57) in 2305.02357
334
+ # The ordering of the array is important
335
+ if len(T) > 1:
336
+ if not T[0] >= T[1]:
337
+ raise errors.PercolationError("T is not decreasing in the percolation integral.")
338
+
339
+ vol_int = np.array([integrate.trapezoid(1 / H[i:], x=T[i:]) for i in range(len(T))])
340
+ with np.errstate(invalid="ignore"):
341
+ integrant = Gamma(T, S) / T**4 / H * vol_int**3
342
+ y = integrate.trapezoid(np.nan_to_num(integrant), x=T)
343
+ return 4 * np.pi / 3 * vw**3 * y
344
+
345
+
346
+ def percIntegralwExp(T: np.ndarray, H: np.ndarray, S: np.ndarray,
347
+ scaleF_ratio: np.ndarray,
348
+ soundSpeedSq: np.ndarray,
349
+ vw=1.0) -> float:
350
+ """Perform the percolation integral beteen Tstart and Tend.
351
+ Take the expansion of the universe into account.
352
+
353
+ Parameters
354
+ ----------
355
+ T : np.ndarray
356
+ Temperature of the dark sector symmetric phase. It must start with the
357
+ nucleation temperature and go down to the percolation temp.
358
+ H : np.ndarray
359
+ Hubble rate evaluated at T
360
+ S : np.ndarray
361
+ S3 Euclidian bounce action evaluated at T
362
+ vw : float
363
+
364
+ Returns
365
+ -------
366
+ float
367
+ The integral evaluated at the last temperature ``T[-1]``.
368
+ """
369
+ # See eq. (4.57) in 2305.02357
370
+ # The ordering of the array is important
371
+ if len(T) > 1:
372
+ if not T[0] >= T[1]:
373
+ raise errors.PercolationError("T is not decreasing in the percolation integral.")
374
+
375
+ vol_int = np.zeros_like(T)
376
+ for i in range(len(T)):
377
+ vol_int[i] = integrate.trapezoid(1/(3*H[i:]*T[i:]*soundSpeedSq[i:]*scaleF_ratio[i:]),
378
+ x=T[i:])
379
+ with np.errstate(invalid="ignore"):
380
+ integrant = Gamma(T, S) * scaleF_ratio**3 / (3*H*T*soundSpeedSq) * vol_int**3
381
+ y = integrate.trapezoid(np.nan_to_num(integrant), x=T)
382
+ return 4 * np.pi / 3 * vw**3 * y
383
+
384
+
385
+ def approxNucleationCriterion(T_DS: float, S: float, pot, phase_sym, phase_bro) -> float:
386
+ r"""
387
+ Calculate the nucleation criterion for a given temperature and action.
388
+ This function computes the nucleation criterion based on the following equation:
389
+
390
+ .. math::
391
+
392
+ \frac{\Gamma}{H^4} = 1
393
+
394
+ where :math:`\Gamma` denotes the bubble nucleation rate and :math:`H`
395
+ is the Hubble parameter during radiation domination.
396
+
397
+ Parameters
398
+ ----------
399
+ T_DS : float
400
+ Temperature of the DS.
401
+ S : float
402
+ Action at temperature TDS
403
+ pot : generic_potential
404
+ The effective potential object
405
+ phase_sym : PhaseInfo
406
+ The information about the high temperature phase
407
+ phase_bro : PhaseInfo
408
+ The information about the low temperature phase
409
+
410
+ Returns
411
+ -------
412
+ float
413
+ Result of the nucleation criterion, 0 if fulfilled.
414
+ """
415
+ T_DS += 1e-100
416
+
417
+ if S == -np.inf:
418
+ return np.inf
419
+ elif S == np.inf:
420
+ return -np.inf
421
+ # This approximation identifies the PT-sector temperature with T_DS and
422
+ # keeps any hidden-sector temperature ratio temperature independent.
423
+ T = T_DS
424
+
425
+ rho = energyDensity(pot, phase_sym, T)
426
+ H = HubbleParameter(rho, pot.conversionFactor)
427
+ logG = logGamma(T_DS, S)[0] # Returnvalue is an array
428
+ crit = logG - 4 * np.log(H)
429
+ # crit is > 0 if more than one bubble is nucleated until T
430
+ # crit is < 0 if less than one bubble is nucleated until T
431
+
432
+ return crit
433
+
434
+
435
+ def ApproxPercCritSecondOrder(
436
+ T: float,
437
+ Tnuc: float,
438
+ outdict: dict,
439
+ pot,
440
+ phase_sym,
441
+ phase_bro,
442
+ Tmax: float,
443
+ Tmin: float,
444
+ vw: float,
445
+ verbose: bool = False,
446
+ ) -> float:
447
+ r"""Calculate a an approximation of the percolation temperature.
448
+ This uses the saddlepoint approximation of
449
+
450
+ .. math::
451
+ \Gamma = \Gamma_p e^{\beta (t - t_p) - \alpha^2 / 2 (t - t_p)^2}
452
+
453
+ Important: The criterion returns a value < 0 if the temperature is too high
454
+ and a value > 0 if the temperature is too small. It returns 0 if the criterion
455
+ is met.
456
+
457
+ If the action becomes inf at a temperature, this has to be because
458
+ the S/T function is u-shaped and must occur at the Tmin end, since
459
+ at Tnuc the action is finite. Therefore also return np.inf for
460
+ S/T = inf.
461
+
462
+ Parameters
463
+ ----------
464
+ T : float
465
+ The temperature.
466
+ Tnuc : float
467
+ The nucleation temperature
468
+ outdict : dict
469
+ Dictionary storing the action evaluation
470
+ pot : generic_potential
471
+ The effective potential object
472
+ phase_sym : PhaseInfo
473
+ The high temperature phase information
474
+ phase_bro : PhaseInfo
475
+ The low temperature phase information
476
+ Tmax : float
477
+ The maximal temperature, where to compute the nucleation criterion (usually Tnuc)
478
+ Tmin : float
479
+ The minimal temperature where to compute the nucleation criterion, usually
480
+ the minimal temp. of coexistence of the phases.
481
+ vw : float
482
+ The bubble wall velocity.
483
+ verbose : bool, optional
484
+ Set the output level
485
+
486
+ Returns
487
+ -------
488
+ float
489
+ Zero if the criterion is met for ``T``; negative if ``T`` is too large, positive if it is too small."""
490
+ fperc = 0.34 # - log(P = 0.7) = 0.34
491
+ dT = np.maximum(T * 1e-3, 1e-3) # Try this for numerical stability
492
+ Tr = np.zeros(3)
493
+ Sr = np.zeros(3)
494
+ logGr = np.zeros(3)
495
+
496
+
497
+ Sr[0] = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
498
+ Sr[1] = calcAction(pot, T, phase_sym, phase_bro, outdict)
499
+ Sr[2] = calcAction(pot, T - dT, phase_sym, phase_bro, outdict)
500
+
501
+ # If S/T is inf then the temperature must be too small
502
+ if np.isinf(Sr[1] / T):
503
+ return np.inf
504
+
505
+ if np.isinf(Sr[0] / T):
506
+ # It could be that the temperature interval was chosen too large
507
+ while np.isinf(Sr[0]) and dT / T > 1e-10:
508
+ dT *= 0.1
509
+ Sr[0] = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
510
+ if np.isinf(Sr[0] / T):
511
+ # If we still have an inf action, then the temperature is too small
512
+ return np.inf
513
+
514
+ Tr[0] = T + dT
515
+ Tr[1] = T
516
+ Tr[2] = T - dT
517
+ logGr[0] = logGamma(Tr[0], Sr[0])
518
+ logGr[1] = logGamma(Tr[1], Sr[1])
519
+ logGr[2] = logGamma(Tr[2], Sr[2])
520
+ rho = energyDensity(pot, phase_sym, T)
521
+ H = HubbleParameter(rho, pot.conversionFactor)
522
+ betaH = -T * (logGr[0] - logGr[1]) / dT
523
+
524
+ dSTdT = (Sr[0] / Tr[0] - Sr[1] / Tr[1]) / dT
525
+ d2STdT2 = (Sr[0] / Tr[0] - 2 * Sr[1] / Tr[1] + Sr[2] / Tr[2]) / dT**2
526
+
527
+ dHdT = (HubbleParameter(energyDensity(pot, phase_sym, T + dT), pot.conversionFactor) - H) / dT
528
+ alphaH2 = -(T**2) * d2STdT2 - (T**2 * dHdT / H + T) * dSTdT
529
+
530
+ beta = betaH * H
531
+ asq = -alphaH2 * H**2
532
+ Iperc = 4 * np.pi / 3 * vw**3
533
+ Iperc *= approxPercIntegral(T, logGr[1], Tnuc, beta, asq, pot, phase_sym, verbose)
534
+ crit = np.log(fperc / (Iperc + 1e-300))
535
+
536
+ return crit
537
+
538
+
539
+ def ApproxPercolCriterion(
540
+ T: float, outdict: dict, pot, phase_sym, phase_bro, Tmax: float, Tmin: float, vw: float, verbose: bool = False
541
+ ) -> float:
542
+ r"""Calculate a rough approximation of the percolation temperature.
543
+ This uses the saddlepoint approximation of
544
+
545
+ .. math::
546
+
547
+ \Gamma = \Gamma_0 \exp\bigl[-\beta (t - t_0)\bigr]
548
+
549
+ Important: The criterion returns a value < 0 if the temperature is too high
550
+ and a value > 0 if the temperature is too small. It returns 0 if the criterion
551
+ is met.
552
+
553
+ If the action becomes inf at a temperature, this has to be because
554
+ the S/T function is u-shaped and must occur at the Tmin end, since
555
+ at Tnuc the action is finite. Therefore also return np.inf for
556
+ S/T = inf.
557
+
558
+ Parameters
559
+ ----------
560
+ T : float
561
+ The temperature.
562
+ outdict : dict
563
+ Dictionary storing the action evaluation
564
+ pot : generic_potential
565
+ The effective potential object
566
+ phase_sym : PhaseInfo
567
+ The high temperature phase information
568
+ phase_bro : PhaseInfo
569
+ The low temperature phase information
570
+ Tmax : float
571
+ The maximal temperature, where to compute the nucleation criterion (usually Tnuc)
572
+ Tmin : float
573
+ The minimal temperature where to compute the nucleation criterion, usually
574
+ the minimal temp. of coexistence of the phases.
575
+ vw : float
576
+ The bubble wall velocity.
577
+ verbose : bool, optional
578
+ Set the output level
579
+
580
+ Returns
581
+ -------
582
+ float
583
+ Zero if the criterion is met for ``T``; negative if ``T`` is too large, positive if it is too small.
584
+ """
585
+ # Check if T is in the range of the potential
586
+ if T >= Tmax:
587
+ return -np.inf
588
+ elif T <= Tmin:
589
+ return np.inf
590
+
591
+ # Calculate beta/H
592
+ fperc = 0.34 # - log(P = 0.7) = 0.34
593
+ dT = np.maximum(T * 1e-2, 1e-3) # Try this for numerical stability
594
+ if T + dT > Tmax:
595
+ dT = (Tmax - T) / 100.0
596
+ Tr = np.zeros(2)
597
+ Sr = np.zeros(2)
598
+ Gr = np.zeros(2)
599
+ Sr[0] = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
600
+ Sr[1] = calcAction(pot, T, phase_sym, phase_bro, outdict)
601
+
602
+ # If S/T is inf then the temperature must be too small
603
+ if np.isinf(Sr[1] / T):
604
+ return np.inf
605
+
606
+ if np.isinf(Sr[0] / T):
607
+ # It could be that the temperature interval was chosen too large
608
+ while np.isinf(Sr[0]) and dT / T > 1e-10:
609
+ dT *= 0.1
610
+ Sr[0] = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
611
+ if np.isinf(Sr[0] / T):
612
+ # If we still have an inf action, then the temperature is too small
613
+ return np.inf
614
+
615
+ Tr[0] = T + dT
616
+ Tr[1] = T
617
+ Gr[0] = Gamma(Tr[0], Sr[0])
618
+ Gr[1] = Gamma(Tr[1], Sr[1])
619
+ rho = energyDensity(pot, phase_sym, T)
620
+ H = HubbleParameter(rho, pot.conversionFactor)
621
+ betaH = -T * (np.log(Gr[0]) - np.log(Gr[1])) / dT
622
+
623
+ # if betaH becomes negative, then the temperature
624
+ # is too small. Return positive infinity
625
+ if betaH < 0:
626
+ return np.inf
627
+
628
+ crit = np.power(Gr[1], 1 / 4.0) / H / betaH / np.power(fperc / (8 * np.pi * vw**3), 1 / 4.0)
629
+ if verbose:
630
+ print(
631
+ f"T = {T * pot.conversionFactor} GeV, S3 = {Sr[1] * pot.conversionFactor} GeV,"
632
+ f"S3/T = {Sr[1] / T}, Gamma = {Gr[1] * pot.conversionFactor**4} GeV^4, betaH = {betaH:2.5g}"
633
+ )
634
+ print("crit = ", crit)
635
+ return crit - 1
636
+
637
+
638
+ def ApproxPercolCriterion2(
639
+ T: float, outdict: dict, pot, phase_sym, phase_bro, Tmax: float, Tmin: float, vw: float, verbose: bool = False
640
+ ) -> float:
641
+ """Alternative method to estimate the percolation criterion. This uses a
642
+ smaller dT for the derivatives and computes betaH from the action not the
643
+ nucleation rate.
644
+
645
+ Parameters
646
+ ----------
647
+ T : float
648
+ The temperature.
649
+ outdict : dict
650
+ Dictionary storing the action evaluation
651
+ pot : generic_potential
652
+ The effective potential object
653
+ phase_sym : PhaseInfo
654
+ The high temperature phase information
655
+ phase_bro : PhaseInfo
656
+ The low temperature phase information
657
+ Tmax : float
658
+ The maximal temperature, where to compute the nucleation criterion (usually Tnuc)
659
+ Tmin : float
660
+ The minimal temperature where to compute the nucleation criterion, usually
661
+ the minimal temp. of coexistence of the phases.
662
+ vw : float
663
+ The bubble wall velocity.
664
+ verbose : bool, optional
665
+ Set the output level
666
+
667
+ Returns
668
+ ----------
669
+ float :
670
+ 0 if the criterion is met for T, < 0 if T is to large, > 0 if T is to small."""
671
+ try:
672
+ T = T[0] # need this when the function is run from optimize.newton, because outdict needs a hashable key
673
+ except Exception:
674
+ pass
675
+
676
+ if T >= Tmax:
677
+ return -np.inf
678
+ if T <= Tmin:
679
+ return np.inf
680
+
681
+ dT = T * 1e-3
682
+ if T + dT > Tmax:
683
+ dT = (Tmax - T) / 100.0
684
+
685
+ if T + dT >= Tmax:
686
+ return -np.inf
687
+ if T + dT <= Tmin:
688
+ return np.inf
689
+
690
+ fperc = 0.34 # - log(P = 0.7) = 0.34
691
+ SdT = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
692
+ S = calcAction(pot, T, phase_sym, phase_bro, outdict)
693
+ if np.isinf(SdT) or np.isinf(S):
694
+ return np.inf
695
+
696
+ if S == 0 or SdT == 0:
697
+ return -np.inf
698
+
699
+ betaH = (SdT - S) / dT - S / T
700
+ rho = energyDensity(pot, phase_sym, T)
701
+ H = HubbleParameter(rho, pot.conversionFactor)
702
+ G = Gamma(T, S)
703
+ if not np.isfinite(betaH) or abs(betaH) < 1e-30:
704
+ if verbose:
705
+ print("Warning: betaH is not finite or zero in ApproxPercolCriterion2. betaH =", betaH)
706
+ return np.inf
707
+ if not np.isfinite(H) or H <= 0:
708
+ if verbose:
709
+ print("Warning: H is not finite or zero in ApproxPercolCriterion2. H =", H)
710
+ return np.inf
711
+ if not np.isfinite(vw) or vw <= 0:
712
+ if verbose:
713
+ print("Warning: vw is not finite or zero in ApproxPercolCriterion2. vw =", vw)
714
+ return np.inf
715
+ crit = np.power(G, 1 / 4.0) / H / betaH / np.power(fperc / (8 * np.pi * vw**3), 1 / 4.0)
716
+ return float(np.squeeze(crit)) - 1
717
+
718
+
719
+ def calcApproxPercolation(
720
+ outdict: dict,
721
+ pot,
722
+ Tnuc: float,
723
+ phase_sym,
724
+ phase_bro,
725
+ vw: float,
726
+ verbose: bool = True,
727
+ tmin: float | None = None,
728
+ tmax: float | None = None,
729
+ ) -> float:
730
+ """Estimate the percolation temperature assuming equal SM/DS temperatures.
731
+
732
+ Parameters
733
+ ----------
734
+ outdict : dict
735
+ Dictionary storing the action evaluation.
736
+ pot : generic_potential
737
+ The effective potential object.
738
+ Tnuc : float
739
+ The nucleation temperature.
740
+ phase_sym : PhaseInfo
741
+ High-temperature phase information.
742
+ phase_bro : PhaseInfo
743
+ Low-temperature phase information.
744
+ vw : float
745
+ Bubble wall velocity.
746
+ verbose : bool, optional
747
+ Whether to emit diagnostic messages.
748
+
749
+ Returns
750
+ -------
751
+ float
752
+ The approximate percolation temperature.
753
+ """
754
+ if tmin is None or tmax is None:
755
+ tmin, tmax = _phase_overlap_interval(pot, phase_sym, phase_bro, Tnuc=Tnuc)
756
+
757
+ dT = np.maximum(Tnuc * 1e-3, 1e-3) # Try this for numerical stability
758
+ Tmin = max(float(tmin) + 2 * dT, float(tmin))
759
+ # Shift Tmax to make derivative possible, take 2*dT to be sure.
760
+ # Keep the search inside the phase-overlap interval.
761
+ Tmax = min(Tnuc - 2 * dT, float(tmax) - 2 * dT)
762
+ if Tmax <= Tmin:
763
+ raise errors.PercolationApproximation1Error(
764
+ "No valid temperature bracket for percolation approximation inside "
765
+ f"the phase-overlap interval [{tmin:.8g}, {tmax:.8g}] at Tnuc={Tnuc:.8g}."
766
+ )
767
+
768
+ try:
769
+ # The calculation at the Tmin failes some times for u-shaped actions
770
+ args = (outdict, pot, phase_sym, phase_bro, Tmax, Tmin, vw, verbose)
771
+ # rtol is large, dont waste time on precision here
772
+ Tperc = optimize.brentq(ApproxPercolCriterion2, Tmax, Tmin, rtol=1e-3, args=args)
773
+
774
+ except ValueError as err:
775
+ if verbose:
776
+ print("Brentq failed in percolation approximation 1: ", err)
777
+ # This error is likely due to the range of the search not being large enough
778
+ # to find the root. We can try to increase the range and see if it works.
779
+ try:
780
+ Tperc = optimize.brentq(
781
+ ApproxPercolCriterion,
782
+ float(tmax),
783
+ Tmin,
784
+ rtol=1e-3,
785
+ args=(outdict, pot, phase_sym, phase_bro, float(tmax), Tmin, vw, verbose),
786
+ )
787
+ except ValueError as err:
788
+ if verbose:
789
+ print("Brentq failed in percolation approximation 1, even after increasing temperature range: ", err)
790
+ # This error may indicate that the error was not due to the range of the search.
791
+ # Try with another method now:
792
+ try:
793
+ Tperc = optimize.newton(
794
+ ApproxPercolCriterion2,
795
+ Tnuc,
796
+ args=(outdict, pot, phase_sym, phase_bro, float(tmax), float(tmin), vw, verbose),
797
+ )
798
+ if verbose:
799
+ print("Newton method used in percolation approximation 1")
800
+ if Tperc < float(tmin) or Tperc > float(tmax):
801
+ raise ValueError("Newton method failed to find a root in the range")
802
+ except Exception as newton_err:
803
+ if verbose:
804
+ print("Newton method failed in percolation approximation 1: ", newton_err)
805
+ raise errors.PercolationApproximation1Error(newton_err)
806
+ return np.squeeze(Tperc)
807
+
808
+
809
+ def Tb_criterion(TBRO: float, TSYM: float, phase_sym, phase_broken, pot) -> float:
810
+ """Criterion to find the broken phase temperature, it
811
+ uses energy conservation.
812
+ Reheat to SM + DS in the broken phase. Assume instant heating.
813
+
814
+ Parameters
815
+ ----------
816
+ TBRO : float
817
+ Broken phase temperature to solve for
818
+ TSYM : float
819
+ Temperature in the symmetric phase
820
+ phase_sym : PhaseInfo
821
+ Field value in the broken phase as a function of temperature
822
+ phase_broken : PhaseInfo
823
+ Field value in the broken phase as a function of temperature
824
+ pot : generic_potential
825
+ The effective potential object
826
+
827
+ Returns
828
+ -------
829
+ float
830
+ Criterion: 0 if ``TBRO`` is the correct broken phase temperature.
831
+ """
832
+
833
+ eSYM = energyDensity(pot, phase_sym, TSYM, include_decoupled=False)
834
+ eBRO = energyDensity(pot, phase_broken, TBRO, include_decoupled=False)
835
+
836
+ return eBRO / eSYM - 1
837
+
838
+
839
+ def energy_criterion_BRO(TBRO: float, eSYM: float, eBRO_start: float, P: float, dP: float, phase_broken, pot) -> float:
840
+ """Calculate the temperature in the broken phase that results
841
+ from the reheating by converting dP of the false vacuum into true vacuum.
842
+
843
+ Parameters
844
+ ----------
845
+ TBRO : float
846
+ Temperature to solve for.
847
+ eSYM : float
848
+ Energy density in the symmetric phase
849
+ eBRO_start : float
850
+ Initial energy density in broken phase
851
+ P : float
852
+ True vacuum fraction
853
+ dP : float
854
+ Change in the true vacuum fraction
855
+ phase_broken : PhaseInfo
856
+ Information about the end phase.
857
+ pot : generic_potential
858
+
859
+ Returns
860
+ -------
861
+ float
862
+ Zero when the criterion is fulfilled.
863
+ """
864
+ eBRO = energyDensity(pot, phase_broken, TBRO, include_decoupled=False)
865
+ crit = eBRO * P - (eBRO_start * (P - dP) + dP * eSYM)
866
+ return crit
867
+
868
+
869
+ def entropy_criterion_SYM_BRO(Tb: float, TBRO_ref: float, TSYM: float, TSYM_ref: float, phase_broken, pot) -> float:
870
+ """Calculate the temperature of the dark sector broken phase in terms
871
+ of the symmetric phase temperature. Only valid when entropy is
872
+ conserved between reference temperature and TSYM.
873
+
874
+ Parameters
875
+ ----------
876
+ Tb : float
877
+ Temperature in the broken phase.
878
+ TBRO_ref : float
879
+ Reference temperature of DS from which on entropy is conserved.
880
+ TSYM : float
881
+ SYM temperature for which we want to know TDS(TSM).
882
+ TSYM_ref : float
883
+ Reference temperature of SYM from which on entropy is conserved.
884
+ phase_broken : PhaseInfo
885
+ Phase information about the end (broken) phase.
886
+ pot : generic_potential
887
+
888
+ Returns
889
+ -------
890
+ float
891
+ Zero when the condition is met.
892
+ """
893
+ heff_ds_pt = h_eff_DS(TBRO_ref, pot, phase_broken)
894
+ heff_SM_pt = td.s_geffSM(TBRO_ref, pot.conversionFactor)
895
+ heff_ds = h_eff_DS(Tb, pot, phase_broken)
896
+ heff_SM = td.s_geffSM(Tb, pot.conversionFactor)
897
+ crit = (heff_ds + heff_SM) * Tb**3
898
+ crit -= (heff_ds_pt + heff_SM_pt) * TBRO_ref**3 * TSYM**3 / TSYM_ref**3
899
+ return crit
900
+
901
+
902
+ @dataclass
903
+ class PercolationSettings:
904
+ """Container for frequently used percolation configuration parameters."""
905
+
906
+ f_perc: float
907
+ f_start: float
908
+ f_final: float
909
+ weight: float
910
+ maxit: int
911
+ rel_increment: float
912
+ max_boundary_n: int
913
+
914
+
915
+ @dataclass
916
+ class PercolationGrid:
917
+ """Shared inputs needed throughout the percolation workflow."""
918
+
919
+ TSYM: np.ndarray
920
+ TpercApprox: float
921
+ tmin: float
922
+ tmax: float
923
+ TBROmin: float
924
+ TBROmax: float
925
+
926
+
927
+ @dataclass
928
+ class PercolationState:
929
+ """Mutable arrays updated during the percolation iterations."""
930
+
931
+ TSYM: np.ndarray
932
+ Sr: np.ndarray
933
+ Hr: np.ndarray
934
+ Pr: np.ndarray
935
+ Pr_exp: np.ndarray
936
+ scalef_ratio: np.ndarray
937
+ soundSpSq: np.ndarray
938
+ Tb: np.ndarray
939
+
940
+
941
+ def _build_percolation_settings(pot, nAction: int) -> PercolationSettings:
942
+ """Read commonly used configuration parameters once."""
943
+ conf = pot.config.percolationConf
944
+ max_boundary_n = int(conf.max_boundary_ratio * nAction)
945
+ return PercolationSettings(
946
+ f_perc=conf.f_perc,
947
+ f_start=conf.f_start,
948
+ f_final=conf.f_final,
949
+ weight=conf.weight,
950
+ maxit=conf.maxit,
951
+ rel_increment=conf.rel_increment,
952
+ max_boundary_n=max_boundary_n,
953
+ )
954
+
955
+
956
+ def _phase_overlap_interval(
957
+ pot,
958
+ phase_symmetric,
959
+ phase_broken,
960
+ Tnuc: float | None = None,
961
+ ) -> tuple[float, float]:
962
+ """Return the common traced temperature interval of both phases.
963
+
964
+ The percolation and observable pipeline assumes all thermodynamic
965
+ quantities are evaluated inside this overlap. If this is violated, we
966
+ surface a hard error instead of silently extrapolating phase splines.
967
+ """
968
+ tmin = max(float(phase_symmetric.Tmin), float(phase_broken.Tmin))
969
+ tmax = min(float(phase_symmetric.Tmax), float(phase_broken.Tmax))
970
+ if not np.isfinite(tmin) or not np.isfinite(tmax) or tmin >= tmax:
971
+ raise errors.PercolationError(
972
+ "No overlapping temperature interval between traced phases: "
973
+ f"Tsym in [{phase_symmetric.Tmin:.8g}, {phase_symmetric.Tmax:.8g}], "
974
+ f"Tbro in [{phase_broken.Tmin:.8g}, {phase_broken.Tmax:.8g}]."
975
+ )
976
+
977
+ if Tnuc is not None:
978
+ tracing_conf = getattr(getattr(pot, "config", None), "tracingConf", None)
979
+ nuc_tol = float(getattr(tracing_conf, "nucleation_Ttol", 1e-8))
980
+ scale = max(abs(tmin), abs(tmax), abs(float(Tnuc)), 1.0)
981
+ temp_tol = max(nuc_tol, 1e-10 * scale)
982
+ if Tnuc < tmin - temp_tol or Tnuc > tmax + temp_tol:
983
+ raise errors.PercolationError(
984
+ "Nucleation temperature is outside the traced phase-overlap interval: "
985
+ f"Tnuc={Tnuc:.8g}, overlap=[{tmin:.8g}, {tmax:.8g}], tol={temp_tol:.3g}. "
986
+ "This indicates inconsistent phase tracing / tunnelling data."
987
+ )
988
+
989
+ return tmin, tmax
990
+
991
+
992
+ def _initial_temperature_grid(
993
+ outdict: dict,
994
+ pot,
995
+ Tnuc: float,
996
+ phase_symmetric,
997
+ phase_broken,
998
+ vw: float,
999
+ nAction: int,
1000
+ settings: PercolationSettings,
1001
+ overlap_tmin: float,
1002
+ overlap_tmax: float,
1003
+ verbose: bool,
1004
+ ) -> PercolationGrid:
1005
+ """Step 1: build the temperature grid around an approximate percolation point."""
1006
+ if verbose:
1007
+ print("\nPercolation step 1: calculating Tperc using the saddlepoint approximation")
1008
+ try:
1009
+ TpercApprox = calcApproxPercolation(
1010
+ outdict,
1011
+ pot,
1012
+ Tnuc,
1013
+ phase_symmetric,
1014
+ phase_broken,
1015
+ vw,
1016
+ verbose,
1017
+ tmin=overlap_tmin,
1018
+ tmax=overlap_tmax,
1019
+ )
1020
+ except errors.PercolationApproximation1Error as err:
1021
+ if verbose:
1022
+ print("Percolation approximation 1 failed: ", err, ". We will continue with TpercApprox = Tnuc.")
1023
+ TpercApprox = Tnuc
1024
+ if verbose:
1025
+ print(f"Approximate percolation temperature: {TpercApprox * pot.conversionFactor:2.5g} GeV")
1026
+
1027
+ dT = (Tnuc - TpercApprox) / int(nAction * settings.weight)
1028
+ tmin = overlap_tmin
1029
+ tmax = overlap_tmax
1030
+
1031
+ if TpercApprox < Tnuc:
1032
+ upper = np.maximum(TpercApprox - int(nAction * (1 - settings.weight)) * dT, tmin)
1033
+ TSYM = np.linspace(Tnuc, upper, nAction)
1034
+ else:
1035
+ if verbose:
1036
+ print("Warning: TpercApprox > Tnuc, use interval around TpercApprox")
1037
+ # here we set couple_hydrodynamics to false, since we are only interested in alpha_tot anyways
1038
+ _, alpha_perc_approx, _, _, _, _ = calcAlphas(TpercApprox, pot, phase_symmetric, phase_broken,
1039
+ coupled_hydrodynamics=False, verbose=verbose)
1040
+
1041
+ # The following is a heuristic to set the temperature range. The values are
1042
+ # chose somewhat arbitrarily, but they should be reasonable for most cases.
1043
+ # This step is not critical, but it should be chosen such that
1044
+ # a percolation temperature is found within a reasonable range.
1045
+ if alpha_perc_approx < 1:
1046
+ Tmin = 0.999 * TpercApprox
1047
+ Tmax = 1.001 * TpercApprox
1048
+ elif alpha_perc_approx < 10:
1049
+ Tmin = 0.9 * TpercApprox
1050
+ Tmax = 1.1 * TpercApprox
1051
+ elif alpha_perc_approx < 100:
1052
+ Tmin = 0.8 * TpercApprox
1053
+ Tmax = 1.2 * TpercApprox
1054
+ else:
1055
+ Tmin = 0.5 * TpercApprox
1056
+ Tmax = 1.5 * TpercApprox
1057
+ Tmax = min(Tmax, tmax)
1058
+ Tmin = max(Tmin, tmin)
1059
+ TSYM = np.linspace(Tmax, Tmin, nAction)
1060
+
1061
+ return PercolationGrid(
1062
+ TSYM=TSYM,
1063
+ TpercApprox=TpercApprox,
1064
+ tmin=tmin,
1065
+ tmax=tmax,
1066
+ TBROmin=phase_broken.Tmin,
1067
+ TBROmax=phase_broken.Tmax,
1068
+ )
1069
+
1070
+
1071
+ def _initial_percolation_scan(
1072
+ grid: PercolationGrid,
1073
+ settings: PercolationSettings,
1074
+ outdict: dict,
1075
+ pot,
1076
+ phase_symmetric,
1077
+ phase_broken,
1078
+ CF: float,
1079
+ vw: float,
1080
+ verbose: bool,
1081
+ ) -> PercolationState:
1082
+ """Step 2: compute P(T) with a P-independent Hubble rate.
1083
+
1084
+ Calculate the action and Hubble parameter at each temperature
1085
+ to find a first guess for the true vacuum fraction P and use the latter
1086
+ to calculate the percolation temperature."""
1087
+ TSYM = grid.TSYM
1088
+ Sr = np.zeros_like(TSYM)
1089
+ Hr = np.zeros_like(TSYM)
1090
+ Pr = np.zeros_like(TSYM)
1091
+ Tb = np.zeros_like(TSYM)
1092
+ scalef_ratio = np.zeros_like(TSYM)
1093
+ soundSpSq = np.zeros_like(TSYM)
1094
+ Pr_exp = np.zeros_like(TSYM)
1095
+ TBROmin = grid.TBROmin
1096
+ TBROmax = grid.TBROmax
1097
+
1098
+ iterator = 1
1099
+ iteration_condition = True
1100
+ if verbose:
1101
+ print("\nPercolation step 2: calculating Tperc assuming P = 0 in the Hubble rate")
1102
+
1103
+ while iteration_condition:
1104
+ Sr.fill(0)
1105
+ Hr.fill(0)
1106
+ Pr.fill(0)
1107
+ Tb.fill(0)
1108
+ Pr_exp.fill(0)
1109
+ soundSpSq.fill(0)
1110
+ scalef_ratio.fill(0)
1111
+ is_Pr_converged = False
1112
+
1113
+ for i, T in enumerate(TSYM):
1114
+ if not is_Pr_converged:
1115
+ Sr[i] = calcAction(pot, T, phase_symmetric, phase_broken, outdict)
1116
+ rho = energyDensity(pot, phase_symmetric, T)
1117
+ scalef_ratio[i] = scalefactorRatio(pot, TSYM[0], T, phase_symmetric.valAt(T))
1118
+ soundSpSq[i] = calcSoundSpeedSq(pot, phase_symmetric.valAt(T), T)
1119
+ Hr[i] = HubbleParameter(rho, CF)
1120
+ Pr[i] = 1 - np.exp(-percIntegral(TSYM[0:i+1], Hr[0:i+1], Sr[0:i+1], vw=vw))
1121
+ Pr_exp[i] = 1 - np.exp(-percIntegralwExp(TSYM[0:i+1], Hr[0:i+1],
1122
+ Sr[0:i+1], scalef_ratio[0:i+1],
1123
+ soundSpSq[0:i+1], vw=vw))
1124
+ else:
1125
+ Sr[i] = np.nan
1126
+ rho = energyDensity(pot, phase_symmetric, T)
1127
+ Hr[i] = HubbleParameter(rho, CF)
1128
+ scalef_ratio[i] = scalefactorRatio(pot, TSYM[0], T, phase_symmetric.valAt(T))
1129
+ soundSpSq[i] = calcSoundSpeedSq(pot, phase_symmetric.valAt(T), T)
1130
+ Pr[i] = Pr[i - 1]
1131
+ Pr_exp[i] = Pr_exp[i - 1]
1132
+
1133
+ if verbose:
1134
+ print(
1135
+ f"Step {i+1}/{len(TSYM)}, T = {TSYM[i] * CF:2.8g} GeV, "
1136
+ f"P_true = {Pr[i]:2.5g}, S_3 = {Sr[i] * CF:2.5g} GeV, "
1137
+ f"S_3/T = {Sr[i] / T:2.5g}"
1138
+ )
1139
+
1140
+ if i >= 1 and Pr[i] > settings.f_final and Pr[i - 1] > settings.f_final:
1141
+ if verbose and not is_Pr_converged:
1142
+ print(
1143
+ "WARNING: P > ",
1144
+ settings.f_final,
1145
+ " at T = ",
1146
+ TSYM[i] * CF,
1147
+ " GeV detected. Stop computation of action from here on "
1148
+ "and only fill in the values for the Hubble rate.",
1149
+ )
1150
+ is_Pr_converged = True
1151
+
1152
+ boundary_n = min(settings.max_boundary_n, len(Pr) - 1)
1153
+ if Pr[0] <= settings.f_start and Pr[-1] >= settings.f_final:
1154
+ # This is good already! P_true goes from < f_start to > f_final.
1155
+ # The temperature range could maybe be improved:
1156
+
1157
+ # Check if Pr includes a jump from 0 to 1
1158
+ jump_indices = np.where((Pr[:-1] == 0) & (Pr[1:] == 1))[0]
1159
+ has_jump_0_to_1 = jump_indices.size > 0
1160
+ if has_jump_0_to_1:
1161
+ # If there is a jump from 0 to 1, adjust the temperature range to cover only this jump.
1162
+ max_index = jump_indices[0]
1163
+ min_index = jump_indices[0] + 1
1164
+ tsym_jump_max = TSYM[max_index]
1165
+ tsym_jump_min = TSYM[min_index]
1166
+ if verbose:
1167
+ print(
1168
+ "Warning: Pr includes a 0 followed by 1, i.e. a "
1169
+ "discontinuous jump in true vacuum fraction."
1170
+ )
1171
+ print(
1172
+ f"Jump occurs between the indices {max_index} and {min_index}, "
1173
+ f"TSYM values: {tsym_jump_min} and {tsym_jump_max}. "
1174
+ "Set these as new boundaries."
1175
+ )
1176
+ if tsym_jump_max / tsym_jump_min - 1 > 1e-10:
1177
+ TSYM = np.linspace(tsym_jump_max, tsym_jump_min, len(TSYM))
1178
+ grid.TSYM = TSYM
1179
+ Sr = np.zeros_like(TSYM)
1180
+ Hr = np.zeros_like(TSYM)
1181
+ Pr = np.zeros_like(TSYM)
1182
+ Tb = np.zeros_like(TSYM)
1183
+ iterator += 1
1184
+ iteration_condition = iterator < settings.maxit
1185
+ else:
1186
+ if verbose:
1187
+ print(
1188
+ "WARNING: Even though there is a jump in P_true, the "
1189
+ "temperature range is already very small. We will not "
1190
+ "adjust it further and call the percolation temperature "
1191
+ "calculation converged."
1192
+ )
1193
+ # The difference in the temperatures is already tiny,
1194
+ # so further shrinking the range would only make the code unstable.
1195
+ iteration_condition = False
1196
+
1197
+ elif Pr[boundary_n] == 0 or Pr[-boundary_n] == 1:
1198
+ # There are too many points in the temperature range where P_true = 0 or P_true = 1.
1199
+ if verbose and iterator != settings.maxit:
1200
+ print(
1201
+ "Warning: the temperature range for the percolation "
1202
+ "temperature computation was chosen too wide in iteration ",
1203
+ iterator,
1204
+ ". Decrease it!",
1205
+ )
1206
+
1207
+ if Pr[boundary_n] == 0:
1208
+ # redo step 2 with a lower highest temperature
1209
+ Tmax_new = TSYM[np.where(Pr < settings.f_start)[0][-1]]
1210
+ else:
1211
+ Tmax_new = TSYM[0]
1212
+
1213
+ if Pr[-boundary_n] == 1:
1214
+ # redo step 2 with a higher lowest temperature
1215
+ Tmin_new = TSYM[np.where(Pr > settings.f_final)[0][0]]
1216
+ else:
1217
+ Tmin_new = TSYM[-1]
1218
+
1219
+ if Tmax_new < Tmin_new:
1220
+ raise errors.PercolationError(
1221
+ "The temperature range for the percolation temperature "
1222
+ "computation was messed up. This should not happen. Please report this as a bug."
1223
+ )
1224
+ TSYM = np.linspace(Tmax_new, Tmin_new, len(TSYM))
1225
+ grid.TSYM = TSYM
1226
+ Sr = np.zeros_like(TSYM)
1227
+ Hr = np.zeros_like(TSYM)
1228
+ Pr = np.zeros_like(TSYM)
1229
+ Tb = np.zeros_like(TSYM)
1230
+ iterator += 1
1231
+ iteration_condition = iterator < settings.maxit
1232
+ else:
1233
+ # The temperature range is good!
1234
+ iteration_condition = False
1235
+ else:
1236
+ # The temperature range is not good, we need to adjust it.
1237
+ if verbose and iterator != settings.maxit:
1238
+ print(
1239
+ "Warning: the temperature range for the percolation "
1240
+ "temperature computation was chosen too narrow in iteration ",
1241
+ iterator,
1242
+ ". Increase it!",
1243
+ )
1244
+ if verbose and iterator == settings.maxit:
1245
+ print(
1246
+ "Warning: the temperature range for the percolation "
1247
+ "temperature computation was chosen too narrow. This is "
1248
+ "the last iteration, so we will use the current "
1249
+ "temperature range anyway."
1250
+ )
1251
+ if Pr[0] > settings.f_start:
1252
+ Tmax_new = TSYM[0] + (grid.tmax - TSYM[0]) * settings.rel_increment
1253
+ else:
1254
+ Tmax_new = TSYM[0]
1255
+
1256
+ if Pr[-1] < settings.f_final:
1257
+ # redo step 2 with a lower lowest temperature
1258
+ if iterator >= settings.maxit - 2:
1259
+ # If we are at the last iteration, we use the tmin value
1260
+ # to be sure that we cover the whole possible temperature
1261
+ # range before concluding that percolation is not possible.
1262
+ Tmin_new = grid.tmin
1263
+ else:
1264
+ Tmin_new = TSYM[-1] - (TSYM[-1] - grid.tmin) * settings.rel_increment
1265
+ else:
1266
+ Tmin_new = TSYM[-1]
1267
+
1268
+ # The temperature must be decreasing in this array
1269
+ TSYM = np.linspace(Tmax_new, Tmin_new, len(TSYM))
1270
+ grid.TSYM = TSYM
1271
+ Sr = np.zeros_like(TSYM)
1272
+ Hr = np.zeros_like(TSYM)
1273
+ Pr = np.zeros_like(TSYM)
1274
+ Tb = np.zeros_like(TSYM)
1275
+ iterator += 1
1276
+ iteration_condition = iterator < settings.maxit
1277
+
1278
+ return PercolationState(TSYM=grid.TSYM, Sr=Sr, Hr=Hr, Pr=Pr, Tb=Tb,
1279
+ Pr_exp=Pr_exp, soundSpSq=soundSpSq, scalef_ratio=scalef_ratio)
1280
+
1281
+
1282
+ def _solve_for_initial_Tperc(
1283
+ state: PercolationState,
1284
+ settings: PercolationSettings,
1285
+ pot,
1286
+ verbose: bool,
1287
+ ) -> float:
1288
+ """Solve for the percolation temperature after step 2."""
1289
+ Pint = interpolate.interp1d(state.TSYM, state.Pr)
1290
+ try:
1291
+ Tperc_prev = optimize.brentq(lambda T: Pint(T) - settings.f_perc, state.TSYM[0], state.TSYM[-1])
1292
+ except ValueError as err:
1293
+ msg = err.args[0]
1294
+ if msg.startswith("f(a) and f(b) must have different signs"):
1295
+ if Pint(state.TSYM[-1]) < settings.f_perc:
1296
+ raise errors.TooMuchSupercoolingError(
1297
+ "The percolation temperature could not be found because the true vacuum "
1298
+ "fraction only reaches "
1299
+ f"{Pint(state.TSYM[-1])} < {settings.f_perc} at Tmin = "
1300
+ f"{state.TSYM[-1] * pot.conversionFactor} GeV."
1301
+ )
1302
+ raise errors.PercolationError(err)
1303
+ except Exception as err:
1304
+ msg = (
1305
+ "Error in calculating Tperc_prev after the second approximation step "
1306
+ "with P = 0 in the Hubble rate. This might be because the nucleation "
1307
+ "criterion could be fulfilled but not the percolation one due to "
1308
+ f"strong supercooling: {err}"
1309
+ )
1310
+ if verbose:
1311
+ print(msg)
1312
+ raise errors.PercolationError(err)
1313
+ if verbose:
1314
+ print(f"Tperc_prev = {Tperc_prev * pot.conversionFactor:2.8g} GeV")
1315
+ return Tperc_prev
1316
+
1317
+
1318
+ def _refine_percolation_temperature(
1319
+ state: PercolationState,
1320
+ grid: PercolationGrid,
1321
+ settings: PercolationSettings,
1322
+ outdict: dict,
1323
+ pot,
1324
+ phase_symmetric,
1325
+ phase_broken,
1326
+ CF: float,
1327
+ vw: float,
1328
+ Tperc_prev: float,
1329
+ rtol: float,
1330
+ verbose: bool,
1331
+ ) -> tuple[float, PercolationState]:
1332
+ """Step 3: iterate with the P-dependent Hubble rate until convergence.
1333
+
1334
+ Refine the percolation temperature by solving the
1335
+ percolation integral with the Hubble rate and the true vacuum fraction
1336
+ calculated in the previous step. The function iteratively
1337
+ improves the true vacuum fraction and the Hubble rate until
1338
+ convergence is reached."""
1339
+ TBROmin = grid.TBROmin
1340
+ TBROmax = grid.TBROmax
1341
+ TSYM = state.TSYM
1342
+ iterator = 1
1343
+ iteration_condition = True
1344
+
1345
+ if verbose:
1346
+ print("\nPercolation step 3: calculating Tperc with P-dependent Hubble rate")
1347
+
1348
+ while iteration_condition:
1349
+ if verbose:
1350
+ print(f"Iteration {iterator}, T_perc = {Tperc_prev * CF:2.8g} GeV")
1351
+
1352
+ Pprev = state.Pr.copy()
1353
+ state.Hr.fill(0)
1354
+ state.Pr.fill(0)
1355
+ state.Pr_exp.fill(0)
1356
+ state.scalef_ratio.fill(0)
1357
+ state.soundSpSq.fill(0)
1358
+ state.Tb.fill(0)
1359
+ state.Sr.fill(0)
1360
+ is_Pr_converged = False
1361
+
1362
+ for i, T in enumerate(TSYM):
1363
+ if not is_Pr_converged:
1364
+ eSYM = energyDensity(pot, phase_symmetric, T)
1365
+ if i == 0: # High temperature bin
1366
+ # Pprev must be zero here
1367
+ state.Hr[i] = HubbleParameter(eSYM, CF)
1368
+ state.Sr[i] = calcAction(pot, T, phase_symmetric, phase_broken, outdict)
1369
+ state.scalef_ratio[i] = 1
1370
+ state.soundSpSq[i] = calcSoundSpeedSq(pot, phase_symmetric.valAt(T), T)
1371
+ try:
1372
+ TBRO = optimize.brentq(
1373
+ Tb_criterion,
1374
+ TBROmin,
1375
+ TBROmax,
1376
+ args=(T, phase_symmetric, phase_broken, pot),
1377
+ )
1378
+ state.Tb[i] = TBRO
1379
+ except ValueError as err:
1380
+ # Sometimes, brentq fails to find a root here. This is usually due to
1381
+ # an inaccuracy in phase tracing, which can be solved by increasing
1382
+ # the diftol parameter in the config file.
1383
+ raise errors.PercolationError(
1384
+ "Error in Tb_criterion: "
1385
+ f"{err}. Please check the input parameters again by hand. "
1386
+ "It might be that the phase tracing is not accurate enough, "
1387
+ "try increasing the diftol parameter in the config file."
1388
+ )
1389
+ eBRO = 0
1390
+ else:
1391
+ # Use entropy conservation in the symmetric phase between two time steps
1392
+ # to compute the broken phase energy density in step i based on the
1393
+ # broken phase energy density in step i-1.
1394
+ TBRO = optimize.brentq(
1395
+ entropy_criterion_SYM_BRO, TBROmin, TBROmax,
1396
+ args=(state.Tb[i - 1], T, TSYM[i - 1], phase_broken, pot)
1397
+ )
1398
+ eBRO = energyDensity(pot, phase_broken, TBRO)
1399
+
1400
+ P = Pprev[i]
1401
+ dP = Pprev[i] - Pprev[i - 1]
1402
+ energy_release = eBRO * (P - dP) + dP * eSYM
1403
+ relative_energy_release = energy_release / eBRO
1404
+ if relative_energy_release < 1e-50:
1405
+ # If the energy release between two steps is too small, we don't need to
1406
+ # calculate the broken phase temperature. The following computation in that
1407
+ # case would just try to find an arbitrary argument T in front of a factor zero
1408
+ # being the energy release. It is thus safer to just set TBRO to the value obtained
1409
+ # in the previous step in the entropy conservation.
1410
+ if verbose:
1411
+ print("Energy release is too small to use energy criterion, using T_bro from entropy step")
1412
+ state.Tb[i] = TBRO
1413
+ else:
1414
+ # Use the energy conservation to find the broken phase temperature
1415
+ # based on the energy release between two steps.
1416
+ try:
1417
+ TBRO = optimize.brentq(
1418
+ energy_criterion_BRO,
1419
+ TBROmin,
1420
+ TBROmax,
1421
+ args=(eSYM, eBRO, Pprev[i], Pprev[i] - Pprev[i - 1], phase_broken, pot),
1422
+ )
1423
+ state.Tb[i] = TBRO
1424
+ except ValueError as err:
1425
+ # Sometimes, the energy criterion fails to find a root,
1426
+ # because the energy release is too small.
1427
+ # We try to limit this case by checking the relative
1428
+ # energy release beforehand, but sometimes it still happens.
1429
+ # In that case, we just use the value from the previous step
1430
+ # and print a warning. This is not critical, as the energy
1431
+ # release is small anyway.
1432
+ if verbose:
1433
+ print(
1434
+ "Warning: energy_criterion failed. This is usually "
1435
+ "because the relative energy release "
1436
+ f"{relative_energy_release} is too small. We set it "
1437
+ "to 0 and continue: ",
1438
+ err,
1439
+ )
1440
+ state.Tb[i] = state.Tb[i - 1]
1441
+ TBRO = state.Tb[i]
1442
+ except Exception as err:
1443
+ raise errors.PercolationError(
1444
+ "Error in energy_criterion_BRO: "
1445
+ f"{err}. Please check this input parameters again by hand."
1446
+ )
1447
+
1448
+ eBRO = energyDensity(pot, phase_broken, TBRO)
1449
+
1450
+ if state.Tb[i] < TSYM[i]:
1451
+ raise ValueError(
1452
+ "There was a problem with the T_bro calculation, "
1453
+ "T_bro < T_sym. This is (usually) not allowed by the "
1454
+ "second law of thermodynamics."
1455
+ )
1456
+
1457
+ state.Sr[i] = calcAction(pot, T, phase_symmetric, phase_broken, outdict)
1458
+ state.Hr[i] = HubbleParameter(Pprev[i] * eBRO + (1 - Pprev[i]) * eSYM, CF)
1459
+ state.soundSpSq[i] = calcSoundSpeedSq(pot, phase_symmetric.valAt(T), T)
1460
+ state.scalef_ratio[i] = scalefactorRatio(pot, TSYM[0], T,
1461
+ phase_symmetric.valAt(T))
1462
+ state.Pr[i] = 1 - np.exp(-percIntegral(TSYM[0:i+1], state.Hr[0:i+1],
1463
+ state.Sr[0:i+1], vw=vw))
1464
+ state.Pr_exp[i] = 1 - np.exp(-percIntegralwExp(TSYM[0:i+1], state.Hr[0:i+1],
1465
+ state.Sr[0:i+1],
1466
+ state.scalef_ratio[0:i+1],
1467
+ state.soundSpSq[0:i+1],
1468
+ vw=vw))
1469
+ else:
1470
+ state.Sr[i] = np.nan
1471
+ rho = energyDensity(pot, phase_symmetric, T)
1472
+ state.soundSpSq[i] = calcSoundSpeedSq(pot, phase_symmetric.valAt(T), T)
1473
+ state.scalef_ratio[i] = scalefactorRatio(pot, TSYM[0], T,
1474
+ phase_symmetric.valAt(T))
1475
+ state.Hr[i] = HubbleParameter(rho, CF)
1476
+ state.Pr[i] = state.Pr[i-1]
1477
+ state.Pr_exp[i] = state.Pr_exp[i-1]
1478
+
1479
+ if verbose:
1480
+ print(
1481
+ f"Step {i+1}/{len(TSYM)}, T = {TSYM[i] * CF:2.8g} GeV, "
1482
+ f"P_true = {state.Pr[i]:2.5g}, S_3 = {state.Sr[i] * CF:2.5g} GeV, "
1483
+ f"S_3/T = {state.Sr[i] / T:2.5g}"
1484
+ )
1485
+
1486
+ if i >= 1 and state.Pr[i] > settings.f_final and state.Pr[i - 1] > settings.f_final:
1487
+ if verbose and not is_Pr_converged:
1488
+ print(
1489
+ "WARNING: P > ",
1490
+ settings.f_final,
1491
+ " at T = ",
1492
+ TSYM[i] * CF,
1493
+ " GeV detected. Stop computation of action from here on "
1494
+ "and only fill in the values for the Hubble rate.",
1495
+ )
1496
+ is_Pr_converged = True
1497
+
1498
+ Pint = interpolate.interp1d(TSYM, state.Pr)
1499
+ Pexpint = interpolate.interp1d(TSYM, state.Pr_exp)
1500
+ try:
1501
+ Tperc = optimize.brentq(lambda T: Pint(T) - settings.f_perc, TSYM[0], TSYM[-1])
1502
+ except ValueError as err:
1503
+ msg = err.args[0]
1504
+ if msg.startswith("f(a) and f(b) must have different signs"):
1505
+ if Pint(TSYM[-1]) < settings.f_perc:
1506
+ raise errors.TooMuchSupercoolingError(
1507
+ "The percolation temperature could not be found because the "
1508
+ "true vacuum fraction only reaches "
1509
+ f"{Pint(TSYM[-1])} < {settings.f_perc} at Tmin = "
1510
+ f"{TSYM[-1] * CF} GeV."
1511
+ )
1512
+ raise errors.PercolationError(err)
1513
+
1514
+ except Exception as err:
1515
+ msg = (
1516
+ "Warning: could not compute Tperc after the third approximation step "
1517
+ "with variable P in Hubble rate: "
1518
+ f"{err}. Set the percolation temperature to the one from the previous step."
1519
+ )
1520
+ if verbose:
1521
+ print(msg)
1522
+ Tperc = Tperc_prev
1523
+
1524
+ if np.abs(Tperc - Tperc_prev) / Tperc < rtol:
1525
+ # A percolation temperature was found!
1526
+ if state.Pr[0] <= settings.f_start and state.Pr[-1] >= settings.f_final:
1527
+ # This is good already! P_true goes from < f_start to > f_final.
1528
+ # The temperature range could maybe be improved:
1529
+
1530
+ # Check if Pr includes a jump from 0 to 1
1531
+ jump_indices = np.where((state.Pr[:-1] == 0) & (state.Pr[1:] == 1))[0]
1532
+ has_jump_0_to_1 = jump_indices.size > 0
1533
+ boundary_n = min(settings.max_boundary_n, len(state.Pr) - 1)
1534
+ if has_jump_0_to_1:
1535
+ # If there is a jump from 0 to 1, we need to adjust the temperature range to cover only this jump.
1536
+ max_index = jump_indices[0]
1537
+ min_index = jump_indices[0] + 1
1538
+ tsym_jump_max = TSYM[max_index]
1539
+ tsym_jump_min = TSYM[min_index]
1540
+ if verbose:
1541
+ print(
1542
+ "Warning: Pr includes a 0 followed by 1, i.e. a "
1543
+ "discontinuous jump in true vacuum fraction."
1544
+ )
1545
+ print(
1546
+ f"Jump occurs between the indices {max_index} and {min_index}, "
1547
+ f"TSYM values: {tsym_jump_min} and {tsym_jump_max}. "
1548
+ "Set these as new boundaries."
1549
+ )
1550
+ if tsym_jump_max / tsym_jump_min - 1 > 1e-10:
1551
+ TSYM = np.linspace(tsym_jump_max, tsym_jump_min, len(TSYM))
1552
+ state.TSYM = TSYM
1553
+ state.Sr = np.zeros_like(TSYM)
1554
+ state.Hr = np.zeros_like(TSYM)
1555
+ state.Pr = np.zeros_like(TSYM)
1556
+ state.Tb = np.zeros_like(TSYM)
1557
+ Tperc_prev = Tperc
1558
+ iterator += 1
1559
+ iteration_condition = iterator < settings.maxit
1560
+ else:
1561
+ if verbose:
1562
+ print(
1563
+ "WARNING: Even though there is a jump in P_true, the "
1564
+ "temperature range is already very small. We will not "
1565
+ "adjust it further and call the percolation temperature "
1566
+ "calculation converged."
1567
+ )
1568
+ print(
1569
+ "\nConverged after "
1570
+ f"{iterator} iterations to Tperc = "
1571
+ f"{Tperc * CF:2.8g} GeV"
1572
+ )
1573
+ # The temperature difference is already tiny,
1574
+ # so further shrinking the range would only make the code unstable.
1575
+ iteration_condition = False
1576
+ elif state.Pr[boundary_n] == 0 or state.Pr[-boundary_n] == 1:
1577
+ # There are too many points in the temperature range where P_true = 0 or P_true = 1.
1578
+ if verbose and iterator != settings.maxit:
1579
+ print(
1580
+ "Warning: the temperature range for the percolation "
1581
+ "temperature computation was chosen too wide in iteration ",
1582
+ iterator,
1583
+ ". Decrease it!",
1584
+ )
1585
+ if state.Pr[boundary_n] == 0:
1586
+ # redo step 2 with a lower highest temperature
1587
+ Tmax_new = TSYM[np.where(state.Pr < settings.f_start)[0][-1]]
1588
+ else:
1589
+ Tmax_new = TSYM[0]
1590
+
1591
+ if state.Pr[-boundary_n] == 1:
1592
+ # redo step 2 with a higher lowest temperature
1593
+ Tmin_new = TSYM[np.where(state.Pr > settings.f_final)[0][0]]
1594
+ else:
1595
+ Tmin_new = TSYM[-1]
1596
+
1597
+ if Tmax_new < Tmin_new:
1598
+ raise errors.PercolationError(
1599
+ "The temperature range for the percolation temperature "
1600
+ "computation was messed up. It is not clear how this could "
1601
+ "have happened."
1602
+ )
1603
+ TSYM = np.linspace(Tmax_new, Tmin_new, len(TSYM))
1604
+ state.TSYM = TSYM
1605
+ state.Sr = np.zeros_like(TSYM)
1606
+ state.Hr = np.zeros_like(TSYM)
1607
+ state.Pr = np.zeros_like(TSYM)
1608
+ state.Tb = np.zeros_like(TSYM)
1609
+ state.Pr_exp = np.zeros_like(TSYM)
1610
+ state.soundSpSq = np.zeros_like(TSYM)
1611
+ state.scalef_ratio = np.zeros_like(TSYM)
1612
+ Tperc_prev = Tperc
1613
+ iterator += 1
1614
+ iteration_condition = iterator < settings.maxit
1615
+ else:
1616
+ # The temperature range is good!
1617
+ if verbose:
1618
+ print(
1619
+ "\nConverged after "
1620
+ f"{iterator} iterations to Tperc = "
1621
+ f"{Tperc * CF:2.8g} GeV"
1622
+ )
1623
+ iteration_condition = False # This breaks the while loop
1624
+ else:
1625
+ # The temperature range is not large enough to find a percolation temperature, we need to increase it
1626
+ if verbose:
1627
+ print(
1628
+ "Warning: even though a percolation temperature was previously "
1629
+ f"found (Tperc = {Tperc * CF:2.8g} GeV), it is not clear "
1630
+ "whether the phase transition can finalize. The temperature "
1631
+ "range will therefore be increased."
1632
+ )
1633
+ print("This was iteration", iterator, "of the third step of the percolation calculation.")
1634
+ if iterator == settings.maxit - 1:
1635
+ print(
1636
+ "This was the penultimate iteration, the temperature range "
1637
+ "will be set to the maximum possible range."
1638
+ )
1639
+ elif iterator == settings.maxit:
1640
+ print("This was the last iteration. We stop here and return an error.")
1641
+ else:
1642
+ print("We will increase the temperature range and try again.")
1643
+
1644
+ if state.Pr[0] > settings.f_start:
1645
+ Tmax_new = TSYM[0] + (grid.tmax - TSYM[0]) * settings.rel_increment
1646
+ else:
1647
+ Tmax_new = TSYM[0]
1648
+
1649
+ if state.Pr[-1] < settings.f_final:
1650
+ if iterator == settings.maxit - 1:
1651
+ # If we are at the last iteration, we set the lower temperature boundary
1652
+ # all the way down to the minimal possible temperature allowed by the
1653
+ # potential. This is to ensure that we do not miss the temperature at which
1654
+ # the transition completes.
1655
+ Tmin_new = grid.tmin
1656
+ elif iterator == settings.maxit:
1657
+ raise errors.PercolationError(
1658
+ "The true vacuum fraction at Tmin = "
1659
+ f"{grid.tmin * CF:2.5g} GeV computed in step 3 "
1660
+ "of the percolation computation is too low for the "
1661
+ "transition to be considered complete: "
1662
+ f"P(Tmin) = {state.Pr[-1]:2.5g}."
1663
+ )
1664
+ else:
1665
+ Tmin_new = TSYM[-1] - (TSYM[-1] - grid.tmin) * settings.rel_increment
1666
+ else:
1667
+ Tmin_new = TSYM[-1]
1668
+
1669
+ TSYM = np.linspace(Tmax_new, Tmin_new, len(TSYM))
1670
+ state.TSYM = TSYM
1671
+ state.Sr = np.zeros_like(TSYM)
1672
+ state.Hr = np.zeros_like(TSYM)
1673
+ state.Pr = np.zeros_like(TSYM)
1674
+ state.Tb = np.zeros_like(TSYM)
1675
+ Tperc_prev = Tperc
1676
+ iterator += 1
1677
+ iteration_condition = iterator <= settings.maxit
1678
+ else:
1679
+ if verbose:
1680
+ print(
1681
+ "Percolation temperature T_perc = ",
1682
+ Tperc,
1683
+ "was found, but it is not close enough to the previous one (",
1684
+ Tperc_prev,
1685
+ ") at iteration ",
1686
+ iterator,
1687
+ ". Iterate one more time with the P(t) from the previous step and see if T_perc converges.",
1688
+ )
1689
+ Tperc_prev = Tperc
1690
+ iterator += 1
1691
+ iteration_condition = iterator <= settings.maxit
1692
+
1693
+ return Tperc, state
1694
+
1695
+
1696
+ def calcPercAndEvolve(outdict: dict, Tnuc: float, phase_symmetric,
1697
+ phase_broken, pot, vw=1.0, rtol=1e-4, nAction=50,
1698
+ verbose=False):
1699
+ """
1700
+ Calculate the percolation temperature iteratively. The function
1701
+ assumes that the dark sector and the SM are in thermal equilibrium
1702
+ during the phase transition. First, the function calculates the
1703
+ approximate percolation temperature using the function
1704
+ calcApproxPercolation assuming P = 0 in the computation of the
1705
+ Hubble parameter and using the saddlepoint approximation, i.e.
1706
+ assuming a quickly growing bubble nucleation rate. Then, it
1707
+ computes the percolation temperature by actually solving the
1708
+ percolation integral, assuming that P = 0 at all times, such that
1709
+ the computation of the Hubble rate is simplified. Next, the
1710
+ function refines the percolation temperature by solving the
1711
+ percolation integral with the Hubble rate with the previously
1712
+ calculated P. The function returns the percolation temperature,
1713
+ if it converged, the symmetric phase temperature, the Hubble
1714
+ parameter, the true vacuum fraction, the broken phase temperature,
1715
+ and the action.
1716
+
1717
+ Parameters
1718
+ ----------
1719
+ outdict : dict
1720
+ Dict storing the tunneling information.
1721
+ Tnuc : float
1722
+ Nucleation temperature of the dark sector
1723
+ phase_symmetric : PhaseInfo
1724
+ Phi of the broken phase as function of TDS
1725
+ phase_broken : PhaseInfo
1726
+ Phi of the broken phase as function of TDS
1727
+ pot : generic_potential
1728
+ The effective potential object
1729
+ vw : float, optional
1730
+ The bubble wall velocity
1731
+ rtol : float, optional
1732
+ Desired relative error in the percolation temperature
1733
+ nAction : int, optional
1734
+ Number of action evaluations
1735
+ verbose : bool, optional
1736
+ Set the output level.
1737
+
1738
+ Returns
1739
+ ----------
1740
+ Tperc : float
1741
+ The percolation temperature in the DS.
1742
+ TSYM : ndarray
1743
+ The symmetric phase temperature from Tnuc til Tperc
1744
+ Hr : ndarray
1745
+ The Hubble parameter from Tnuc til Tperc
1746
+ Pr : ndarray
1747
+ The true vacuum fraction from Tnuc til Tperc
1748
+ Tb : ndarray
1749
+ The broken phase temperature (also SM temperature)
1750
+ Sr : ndarray
1751
+ The action from Tnuc til Tperc"""
1752
+
1753
+ CF = pot.conversionFactor
1754
+ settings = _build_percolation_settings(pot, nAction)
1755
+ overlap_tmin, overlap_tmax = _phase_overlap_interval(
1756
+ pot,
1757
+ phase_symmetric,
1758
+ phase_broken,
1759
+ Tnuc=Tnuc,
1760
+ )
1761
+ grid = _initial_temperature_grid(
1762
+ outdict,
1763
+ pot,
1764
+ Tnuc,
1765
+ phase_symmetric,
1766
+ phase_broken,
1767
+ vw,
1768
+ nAction,
1769
+ settings,
1770
+ overlap_tmin,
1771
+ overlap_tmax,
1772
+ verbose,
1773
+ )
1774
+ state = _initial_percolation_scan(
1775
+ grid,
1776
+ settings,
1777
+ outdict,
1778
+ pot,
1779
+ phase_symmetric,
1780
+ phase_broken,
1781
+ CF,
1782
+ vw,
1783
+ verbose,
1784
+ )
1785
+ Tperc_prev = _solve_for_initial_Tperc(state, settings, pot, verbose)
1786
+ Tperc, state = _refine_percolation_temperature(
1787
+ state,
1788
+ grid,
1789
+ settings,
1790
+ outdict,
1791
+ pot,
1792
+ phase_symmetric,
1793
+ phase_broken,
1794
+ CF,
1795
+ vw,
1796
+ Tperc_prev,
1797
+ rtol,
1798
+ verbose,
1799
+ )
1800
+ return Tperc, state.TSYM, state.Hr, state.Pr, state.Tb, state.Sr, \
1801
+ state.Pr_exp, state.scalef_ratio, state.soundSpSq
1802
+
1803
+
1804
+ def calc_betaH_S3(T: float, Sint: interpolate.interp1d, outdict: dict, pot, phase_sym, phase_bro, verbose=False) -> float:
1805
+ """Calculate the phase transition speed from the action derivative.
1806
+
1807
+ Parameters
1808
+ ----------
1809
+ T : float
1810
+ The temperature at which to evaluate beta/H
1811
+ Sint: interpolate.interp1d
1812
+ Interpolation function of the action
1813
+ outdict : dict
1814
+ Dictionary storing the action evaluations, key is `T`
1815
+ pot : generic_potential
1816
+ The effective potential object
1817
+ phase_sym : PhaseInfo
1818
+ Information about the high temperature (symmetric) phase
1819
+ phase_bro : PhaseInfo
1820
+ Information about the low temperature (broken) phaes
1821
+
1822
+ Returns
1823
+ ----------
1824
+ float :
1825
+ The transition speed beta/H."""
1826
+ dT = T * 1e-5
1827
+ tmin = max(float(phase_sym.Tmin), float(phase_bro.Tmin))
1828
+ tmax = min(float(phase_sym.Tmax), float(phase_bro.Tmax))
1829
+ if tmin >= tmax or T <= tmin or T >= tmax:
1830
+ if verbose:
1831
+ print(
1832
+ "Warning: cannot evaluate betaH outside phase overlap. "
1833
+ f"T={T:.8g}, overlap=[{tmin:.8g}, {tmax:.8g}]"
1834
+ )
1835
+ return np.nan
1836
+ # Check if the derivative can actually be calculated
1837
+ # in the range of interest. If not, make the points
1838
+ # support for the derivative closer to each other.
1839
+ if T - 2 * dT < tmin or T + 2 * dT > tmax:
1840
+ dT = np.minimum((T - tmin) / 100, (tmax - T) / 100)
1841
+
1842
+ if len(Sint.x) >= 10 and Sint.x[0] < T - 2 * dT and Sint.x[-1] > T + 2 * dT:
1843
+ dSdT = derivative(Sint, T, dT)
1844
+ betaH = dSdT - Sint(T) / T
1845
+ else:
1846
+ # not enough evaluations of S for accuracy
1847
+ Tr = []
1848
+ Sr = []
1849
+ for i in range(-2, 3, 1):
1850
+ S = calcAction(pot, T - dT * i, phase_sym, phase_bro, outdict)
1851
+ Tr.append(T - dT * i)
1852
+ Sr.append(S)
1853
+ T_ar = np.array(Tr[::-1]) # We need increasing T for the spline interpolation
1854
+ S_ar = np.array(Sr[::-1])
1855
+ if np.all(np.isinf(S_ar)):
1856
+ if verbose:
1857
+ print("WARNING: All elements of S_ar are infinite. Unable to proceed with calculation on beta/H.")
1858
+ print(
1859
+ "Most likely the transition is so weak that the action is infinite. "
1860
+ "Correspondingly, beta/H is also set to inf."
1861
+ )
1862
+ return np.inf
1863
+ tckS = interpolate.splrep(T_ar, S_ar, s=0)
1864
+ S = interpolate.splev(T, tckS, der=0)
1865
+ dSdT = interpolate.splev(T, tckS, der=1)
1866
+ betaH = dSdT - S / T
1867
+ return betaH
1868
+
1869
+
1870
+ def calcSoundSpeedSq(pot, X, T) -> float:
1871
+ """Compute the sound speed squared.
1872
+
1873
+ Note: Decoupled degrees of freedom do not enter here.
1874
+
1875
+ Parameters
1876
+ ----------
1877
+
1878
+ Returns
1879
+ -------
1880
+
1881
+ """
1882
+ dT = T*1e-3
1883
+ dVdT = pot.dVdT(X, T, dT=dT, include_decoupled=False)
1884
+ d2VdT2 = pot.d2VdT2(X, T, dT=dT, include_decoupled=False)
1885
+ csSq = dVdT/(T*d2VdT2)
1886
+ return csSq
1887
+
1888
+
1889
+ def calcAlphas(T: float, pot, high_phase, low_phase, verbose=False) -> tuple[float]:
1890
+ """Calculate the total transition strenght of the PT.
1891
+ Use several definitions.
1892
+
1893
+ The last 3 alphas are normalised to the radiation energy density
1894
+ of only the relevant sector for the bubble expansion. I.e.
1895
+ when `pot.kin_coupled_e/p_geff = 0`, they are normalised to
1896
+ radiation energy density in the PT sector only.
1897
+
1898
+ Parameters
1899
+ ----------
1900
+ T : float
1901
+ Temperature at which to evaluate alpha
1902
+ pot : generic_potential
1903
+ Effective potential
1904
+ high_phase : phaseInfo
1905
+ Phase info of the initial phase
1906
+ low_phase : phaseInfo
1907
+ Phase info of the end phase
1908
+
1909
+ Returns
1910
+ ----------
1911
+ tuple : float
1912
+ Tuple of floats, the 3 definitions of alpha and the alphas used for the
1913
+ calculation the efficency factors kappa."""
1914
+
1915
+ high_phi = high_phase.valAt(T) # Start phase phi values
1916
+ low_phi = low_phase.valAt(T) # End phase phi values
1917
+ DeltaV = np.abs(pot.Vtot(high_phi, T) - pot.Vtot(low_phi, T))
1918
+
1919
+ # Derivative of the potential with respect to T
1920
+ dT = T * 1e-5
1921
+ dDeltaV_p = np.abs(pot.Vtot(high_phi, T + dT / 2) - pot.Vtot(low_phi, T + dT / 2))
1922
+ dDeltaV_m = np.abs(pot.Vtot(high_phi, T - dT / 2) - pot.Vtot(low_phi, T - dT / 2))
1923
+ dDeltaVdT = (dDeltaV_p - dDeltaV_m) / dT
1924
+
1925
+ # previously we used the broken phase geff of the dark sector,
1926
+ # This seemed wrong to me, so I put here the symmetric phase
1927
+ # Note that this gives slightly different alpha values for T < 100 MeV
1928
+
1929
+ # Rad. energy density of the sector with the PT
1930
+ rho_rad_PTsector = pot.radiationEnergyDensity(high_phi, T, include_decoupled=False)
1931
+ # Note: include_decoupled = False means that a secluded sector is not included
1932
+
1933
+ # Here we assume that the decoupled sector has the same temperature as the PT sector
1934
+ rho_rad_tot = pot.radiationEnergyDensity(high_phi, T, include_decoupled=True)
1935
+
1936
+ # Energy density:
1937
+ DeltaE = DeltaV - T * dDeltaVdT
1938
+
1939
+ # Alpha definitions:
1940
+ alpha_p = DeltaV / rho_rad_tot
1941
+ # Evaluate the enthalpy-normalized trace-anomaly definition separately
1942
+ # from the bag-model alpha used above.
1943
+ csSq_sym = calcSoundSpeedSq(pot, high_phi, T)
1944
+ V0 = pot.Vtot(pot.X0, pot.Tmin)
1945
+ theta_sym = pot.energyDensity(high_phi, T) + (pot.Vtot(high_phi, T)- V0)/csSq_sym
1946
+ csSq_bro = calcSoundSpeedSq(pot, low_phi, T)
1947
+ theta_bro = pot.energyDensity(low_phi, T) + (pot.Vtot(low_phi, T) - V0)/csSq_bro
1948
+ alpha_theta = (theta_sym - theta_bro)/(3 * (-pot.Vtot(high_phi, T) + V0 + pot.energyDensity(high_phi, T)))
1949
+
1950
+
1951
+ alpha_e = DeltaE / rho_rad_tot
1952
+
1953
+ bosons_low = pot.boson_massSq(low_phi, 0) # low-T phase masses
1954
+ bosons_high = pot.boson_massSq(high_phi, 0) # high-T phase masses
1955
+ fermions_low = pot.fermion_massSq(low_phi)
1956
+ fermions_high = pot.fermion_massSq(high_phi)
1957
+
1958
+ # alpha_inf
1959
+ gauge_coupling = pot.mass_spectrum.boson_gauge_couplings
1960
+ m2_bos_after, dof_bos, _, is_physical = bosons_low
1961
+ m2_bos_before, _, _, _ = bosons_high
1962
+ m2_fer_after, dof_fer = fermions_low
1963
+ m2_fer_before, _ = fermions_high
1964
+
1965
+ delta_m2_bos = np.maximum(m2_bos_after - m2_bos_before, 0)
1966
+ m2factor = np.sum(dof_bos * is_physical * delta_m2_bos, axis=-1) / 24.0
1967
+
1968
+ delta_m2_fer = np.maximum(m2_fer_after - m2_fer_before, 0)
1969
+ m2factor += np.sum(dof_fer * delta_m2_fer, axis=-1) / 48.0
1970
+
1971
+ m_bos_after = np.sqrt(np.where(m2_bos_after > 0, m2_bos_after, 0))
1972
+ # Avoid sqrt of negative mass squares by setting negatives to zero.
1973
+ # This only occurs for Goldstones, which are excluded via is_physical = 0.
1974
+ m_bos_before = np.sqrt(np.where(m2_bos_before > 0, m2_bos_before, 0))
1975
+ delta_m_bos = np.maximum(m_bos_after - m_bos_before, 0)
1976
+
1977
+ # alpha_eq, see eq. (2.16) in 1903.09642
1978
+ # the hydrodynamic alphas do not depend on the decoupled radiation bath!
1979
+ alpha_eq = T**3 / rho_rad_PTsector * np.sum(delta_m_bos * gauge_coupling**2 * dof_bos * is_physical, axis=-1)
1980
+ alpha_inf = T**2 / (24 * rho_rad_PTsector) * m2factor
1981
+ # alpha_hyd = (DeltaE + 3 * DeltaV) / (4 * rho_rad_PTsector)
1982
+ alpha_hyd = (theta_sym - theta_bro) / \
1983
+ (3 * (V0-pot.Vtot(high_phi, T, include_decoupled=False) +
1984
+ pot.energyDensity(high_phi, T, include_decoupled=False)))
1985
+
1986
+ return alpha_p, alpha_theta, alpha_e, alpha_hyd, alpha_inf, alpha_eq
1987
+
1988
+
1989
+ def calc_betaH_S3_approx(T, outdict, pot, phase_sym, phase_bro, tmin, tmax, verbose=False):
1990
+ """
1991
+ Calculate the betaH parameter at temperature T
1992
+ using the derivative of the bounce action with respect to
1993
+ the temperature.
1994
+ """
1995
+ S = calcAction(pot, T, phase_sym, phase_bro, outdict)
1996
+ if np.isinf(S):
1997
+ if verbose:
1998
+ print("Warning: S(T) is inf")
1999
+ return np.nan
2000
+
2001
+ dT = T * 1e-3
2002
+ # if dT is too large, set it to a smaller value
2003
+ if T + dT > tmax:
2004
+ dT = (tmax - T) / 100
2005
+
2006
+ # Try to calculate the derivative to the right of T
2007
+ SdT = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
2008
+ betaH = (SdT - S) / dT - S / T
2009
+ while np.isinf(SdT) and dT / T > 1e-15:
2010
+ if verbose:
2011
+ print("SdT is inf, reducing dT to ", dT, " and trying again")
2012
+ dT *= 0.1
2013
+ SdT = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
2014
+ betaH = (SdT - S) / dT - S / T
2015
+
2016
+ # if SdT is inf, we cannot calculate betaH
2017
+ # if beta/H is negative, there was a numerical issue
2018
+ if np.isinf(SdT) or betaH < 0:
2019
+ # Try to calculate the derivative to the left of T
2020
+ dT = T * 1e-3
2021
+ if T - dT < tmin:
2022
+ dT = (T - tmin) / 100
2023
+ SdT_ = calcAction(pot, T - dT, phase_sym, phase_bro, outdict)
2024
+ betaH = (S - SdT_) / dT - S / T
2025
+ while np.isinf(SdT_) and dT / T > 1e-15:
2026
+ if verbose:
2027
+ print("SdT is still inf, reducing dT to ", dT, " and trying again")
2028
+ dT *= 0.1
2029
+ SdT_ = calcAction(pot, T - dT, phase_sym, phase_bro, outdict)
2030
+ betaH = (S - SdT_) / dT - S / T
2031
+ if np.isinf(SdT_):
2032
+ if verbose:
2033
+ print("Warning: SdT_ is inf, cannot calculate betaH")
2034
+ return np.nan
2035
+
2036
+ if np.isinf(SdT):
2037
+ if verbose:
2038
+ print("Warning: S(T+dT) is inf, cannot calculate betaH")
2039
+ return np.nan
2040
+
2041
+ if betaH <= 0 and verbose:
2042
+ print("Warning: betaH is negative!")
2043
+
2044
+ return betaH
2045
+
2046
+
2047
+ def calcMeanBubbleSeparation(T, Tmax, Sint, Pint, Hint):
2048
+ """Calculate the mean bubble separation at temperature T.
2049
+
2050
+ Parameters
2051
+ ----------
2052
+ T : float
2053
+ Symmetric phase temperature
2054
+ Tmax : float
2055
+ Upper limit of the integral, usually the nucleation temperature
2056
+ phase_sym : scipy.interpolate.interp1d object
2057
+ The symmetric phase
2058
+ Sint : scipy.interpolate.interp1d object
2059
+ The action
2060
+ Pint : scipy.interpolate.interp1d object
2061
+ The true vacuum fraction.
2062
+ Hint : scipy.interpolate.interp1d object
2063
+ The Hubble rate
2064
+ Returns
2065
+ ----------
2066
+ R : float
2067
+ Mean bubble separation."""
2068
+
2069
+ # See eq. (5.42) in 2305.02357
2070
+ Tr = np.linspace(T, Tmax, 10_000)
2071
+ integrant = Gamma(Tr, Sint(Tr)) * (1 - Pint(Tr)) / (Tr * Hint(Tr)) * (T / Tr) ** 3
2072
+ integrant[np.isnan(integrant)] = 0 # Replace NaNs (due to infinite action) with 0
2073
+ res = integrate.trapezoid(integrant, x=Tr)
2074
+ res = np.power(res, -1 / 3)
2075
+ return res
2076
+
2077
+
2078
+ def calcMeanBubbleSeparationwExp(T, Tmax, Sint, Pint, Hint, CSint, SFRint):
2079
+ """Calculate the mean bubble separation at temperature T.
2080
+ Include the effect of cosmic expansion.
2081
+
2082
+ Parameters
2083
+ ----------
2084
+ T : float
2085
+ Symmetric phase temperature
2086
+ Tmax : float
2087
+ Upper limit of the integral, usually the nucleation temperature
2088
+ phase_sym : scipy.interpolate.interp1d object
2089
+ The symmetric phase
2090
+ Sint : scipy.interpolate.interp1d object
2091
+ The action
2092
+ Pint : scipy.interpolate.interp1d object
2093
+ The true vacuum fraction.
2094
+ Hint : scipy.interpolate.interp1d object
2095
+ The Hubble rate
2096
+ CSint: scipy.interpolate.interp1d object
2097
+ The sound speed squared
2098
+ SRFint: scipy.interpolate.interp1d object
2099
+ The scale factor ratio
2100
+ Returns
2101
+ ----------
2102
+ R : float
2103
+ Mean bubble separation."""
2104
+
2105
+ # See eq. (5.42) in 2305.02357
2106
+ Tr = np.linspace(T, Tmax, 10_000)
2107
+ integrant = Gamma(Tr, Sint(Tr)) * (1 - Pint(Tr)) / (3 * Hint(Tr) * Tr * CSint(Tr) ) \
2108
+ * SFRint(Tr)**3
2109
+ integrant[np.isnan(integrant)] = 0 # Replace NaNs (due to infinite action) with 0
2110
+ res = integrate.trapezoid(integrant, x=Tr)
2111
+ res = np.power(res, -1 / 3)
2112
+ return res
2113
+
2114
+
2115
+
2116
+ def calcTf(Tperc, Tlow, Pint, pot, verbose=False):
2117
+ """Calculate the final temperature at which the transition ends.
2118
+
2119
+ Parameters
2120
+ ----------
2121
+ Tperc : float
2122
+ Symmetric phase percolation temperature, i.e. maximally possible value of the final temperature
2123
+ Tlow : float
2124
+ Lowest temperature considered in the percolation computation. At this temperature, the true vacuum
2125
+ fraction Pint is >= f_final ~ 0.99. The final temperature must thus be <= Tlow.
2126
+ phase_sym : scipy.interpolate.interp1d object
2127
+ The symmetric phase
2128
+ Sint : scipy.interpolate.interp1d object
2129
+ The action
2130
+ Pint : scipy.interpolate.interp1d object
2131
+ The true vacuum fraction.
2132
+ Returns
2133
+ ----------
2134
+ Tf : float
2135
+ Final temperature."""
2136
+
2137
+ f_final = pot.config.percolationConf.f_final # true vacuum fraction at final temperature
2138
+
2139
+ # Step 1: Check if the true vacuum fraction at Tlow is >= f_final
2140
+ if Pint(Tlow) < f_final:
2141
+ # It looks like the transition cannot completely finish
2142
+ msg = "The transition cannot completely finish, Pint(Tlow) < f_final: " + str(Pint(Tlow)) + " < " + str(f_final)
2143
+ raise errors.EternalInflationError(msg)
2144
+
2145
+ # Step 2: Find Tf by solving Pint(Tf) = f_final
2146
+ try:
2147
+ Tf = optimize.brentq(lambda TSYM: Pint(TSYM) - f_final, Tlow, Tperc)
2148
+ except Exception as e:
2149
+ msg = "Error in calculating Tf: " + str(e)
2150
+ if verbose:
2151
+ print(msg)
2152
+ raise errors.PercolationError(e)
2153
+
2154
+ return Tf
2155
+
2156
+
2157
+ # ==================================================
2158
+ # Nucleation computation
2159
+ # ==================================================
2160
+
2161
+
2162
+ def findTunnelingTmax(DV: callable, Tmin: float, Tmax: float, tol=1e-15, maxiter=100):
2163
+ """Finds the largest temperature Tmax in [Tmin, Tmax] such that
2164
+ V(start_phase.valAt(T), T) > V(end_phase.valAt(T), T).
2165
+
2166
+ Important: `tol=1e-15` is required to to find the right Tmax,
2167
+ `tol=1e-8` is not enough!
2168
+
2169
+ Parameters
2170
+ ----------
2171
+ DV : callable
2172
+ Tmin : float
2173
+ Tmax : float
2174
+ tol : float, optional
2175
+ maxiter : int, optional
2176
+
2177
+ Returns
2178
+ -------
2179
+ float
2180
+ The maximal temperature where V(start_phase, Tmax) > V(end_phase, Tmax).
2181
+ """
2182
+
2183
+ if DV(Tmax) > 0:
2184
+ return Tmax
2185
+ # Search for the root of DV(T) = 0 in [Tmin, Tmax]
2186
+ try:
2187
+ Tmax_new = optimize.brentq(DV, Tmin, Tmax, xtol=tol, maxiter=maxiter)
2188
+ return Tmax_new
2189
+ except ValueError:
2190
+ # Maybe DV(Tmin) < 0 for numerical reasons:
2191
+ try:
2192
+ Tmax_new = optimize.brentq(DV, (Tmin + Tmax)/2.0, Tmax, xtol=tol, maxiter=maxiter)
2193
+ return Tmax_new
2194
+ except ValueError:
2195
+ # No crossing found; fall back to Tmin.
2196
+ return Tmin
2197
+
2198
+
2199
+ def findTminFromUShapedNucl(Tmin, Tmax, args, start_phase, end_phase, outdict,
2200
+ Ttol, maxiter, CF, verbose, nuclCriterion):
2201
+ """Find the minimum of the nucleation criterion between `Tmin` and `Tmax`
2202
+
2203
+ Parameters
2204
+ ----------
2205
+
2206
+ Returns
2207
+ -------
2208
+ float | None:
2209
+ Temperature of the minimum of the nucleation criterion.
2210
+ If it is a hidden second order transition, return None
2211
+ """
2212
+ ntest = 100
2213
+ Tmin_new = 0
2214
+ try:
2215
+ # Try to find the minimum of the tunneling action
2216
+ # between Tmin and Tmax
2217
+ def maximizer(x, *args):
2218
+ # This function is used to find the maximum of the
2219
+ # nucleation criterion, corresponding to the minimum of
2220
+ # a U-shaped action.
2221
+ return -evalNucleationCriterion(x, *args)
2222
+
2223
+ Ttest = np.nan
2224
+
2225
+ # This skips Tmin and Tmax, which have already been checked and would
2226
+ # yield boundary errors in the bracketing algorithm
2227
+ Trange = np.linspace(Tmin, Tmax, ntest, endpoint=False)[1:]
2228
+ for t in Trange:
2229
+ nc = evalNucleationCriterion(t, *args)
2230
+ if not np.isinf(nc):
2231
+ Ttest = t
2232
+ # usually this it the case after one or two iterations
2233
+ break
2234
+
2235
+ if np.isnan(Ttest):
2236
+ # It could be that the action is actually very hard to compute
2237
+ # because the transitions is extremely weak. In that case the
2238
+ # action jumps between -inf and +inf. We can then take the highest
2239
+ # temperature for which the nucleation criterion was found to be
2240
+ # fulfilled, even if it is infinitely large due to the numerical
2241
+ # precision.
2242
+ if any([outdict[T]["trantype"] == 2 for T in outdict]):
2243
+ max_key = max(T for T in outdict if outdict[T]["trantype"] == 2)
2244
+ if verbose:
2245
+ print(
2246
+ "Found a second-order transition at T = ",
2247
+ max_key * CF,
2248
+ "GeV. This was a little hard to find because it was "
2249
+ "treated as a very weak first-order transition.",
2250
+ )
2251
+ rdict = outdict[max_key]
2252
+ rdict["Tcrit"] = rdict["Tnuc"] = max_key
2253
+ return None
2254
+
2255
+ if all([evalNucleationCriterion(T, *args) < 0 if T <= Tmax and T >= Tmin else True for T in outdict]):
2256
+ # This means the nucleation criterion is never fulfilled between Tmin and Tmax.
2257
+ # The barrier is too high to allow for the nucleation to be possible.
2258
+ raise errors.NucleationError("Nucleation criterion failed: too low at all temperatures.")
2259
+ raise errors.NucleationError(
2260
+ "Nucleation criterion failed due to unknown reason: No finite nucleation "
2261
+ "criterion found in the range between Tmin and Tmax."
2262
+ )
2263
+ result = optimize.minimize_scalar(
2264
+ maximizer,
2265
+ bracket=(Tmin, Ttest, Tmax),
2266
+ args=args,
2267
+ method="brent",
2268
+ options={"xtol": (Tmax - Tmin) * 1e-5, "maxiter": maxiter, "disp": 0},
2269
+ )
2270
+ if result.success:
2271
+ # We found a minimum of the action! Let's check if the nucleation
2272
+ # criterion is fulfilled at this temperature.
2273
+ Tmin_u = float(np.squeeze(result.x)) # this is a hack to make the np array hashable
2274
+ action_u = outdict[Tmin_u]["action"]
2275
+ ncMin_u = nuclCriterion(action_u, Tmin_u, start_phase, end_phase)
2276
+
2277
+ Tmin_new = Tmin_u
2278
+
2279
+ if verbose:
2280
+ print("Finding Tmin_u done! Tmin_u = ", result.x * CF, "GeV")
2281
+ # Note: it can also happen that a minimum is found in
2282
+ # the range of Tmin and Tmax, even though there is no
2283
+ # actual minimum of the action. This can happen if the
2284
+ # action is u-shaped and so large that the nucleation
2285
+ # criterion is not fulfilled at all. In this case, one
2286
+ # still obtains a minimum of the action at an arbitrary
2287
+ # temperature because the nucleation criterion evaluates
2288
+ # to - inf everywhere, such that the "maximizer" is inf
2289
+ # everywhere. This is not a problem, because in any case
2290
+ # we are checking again if the nucleation criterion is fulfilled
2291
+ # at the found minimum of the action.
2292
+
2293
+ if verbose:
2294
+ print("action_u = ", action_u * CF, "GeV")
2295
+ print("ncMin_u = ", ncMin_u)
2296
+ if ncMin_u < 0:
2297
+ if verbose:
2298
+ print("Nucleation criterion not fulfilled at the minimum of the u-shaped action.")
2299
+ raise errors.NucleationError(
2300
+ "Nucleation criterion failed because the nucleation rate is too low at all temperatures."
2301
+ )
2302
+ if verbose:
2303
+ print("Nucleation criterion fulfilled at the minimum of the u-shaped action.")
2304
+ else:
2305
+ if verbose:
2306
+ print("Finding Tmin_u failed: ", result.message)
2307
+ raise errors.NucleationError("Nucleation criterion failed in the finding of Tmin_u:" + str(result.message))
2308
+ except Exception as err:
2309
+ raise errors.NucleationError("Nucleation calculation failed: " + str(err))
2310
+
2311
+ return Tmin_new
2312
+
2313
+
2314
+ def computeNucleationTemperature(V, dV, d2V, start_phase, end_phase,
2315
+ CF, Ttol=1e-8, maxiter=100, phitol=1e-8,
2316
+ overlapAngle=45.0, nuclCriterion=lambda S, T: 140 - S / (T + 1e-100),
2317
+ approximate_strength_threshold=1e-4, verbose=False,
2318
+ fullTunneling_params={}):
2319
+ """Find the instanton and nucleation temperature for tunneling from
2320
+ `start_phase` to `end_phase`.
2321
+
2322
+ Parameters
2323
+ ----------
2324
+ V, dV : callable
2325
+ The potential V(x,T) and its gradient.
2326
+ start_phase : PhaseInfo
2327
+ The metastable phase from which tunneling occurs.
2328
+ end_phase : PhaesInfo
2329
+ The destination phase.
2330
+ Tmax : float
2331
+ The highest temperature at which to try tunneling.
2332
+ Ttol : float, optional
2333
+ Tolerance for finding the nucleation temperature.
2334
+ maxiter : int, optional
2335
+ Maximum number of times to try tunneling.
2336
+ phitol : float, optional
2337
+ Tolerance for finding the minima.
2338
+ overlapAngle : float, optional
2339
+ If two phases are in the same direction, only try tunneling to the
2340
+ closer one. Set to zero to always try tunneling to all available phases.
2341
+ nuclCriterion : callable
2342
+ Function of the action *S* and temperature *T*. Should return 0 for the
2343
+ correct nucleation rate, > 0 for a low rate and < 0 for a high rate.
2344
+ Defaults to ``S/T - 140``.
2345
+ verbose : bool
2346
+ If true, print a message before each attempted tunneling.
2347
+ fullTunneling_params : dict
2348
+ Parameters to pass to :func:`pathDeformation.fullTunneling`.
2349
+
2350
+ Returns
2351
+ -------
2352
+ dict or None
2353
+ A description of the tunneling solution at the nucleation temperature,
2354
+ or None if there is no found solution. Has the following keys:
2355
+
2356
+ - *Tnuc* : the nucleation temperature
2357
+ - *low_vev, high_vev* : vevs for the low-T phase (the phase that the
2358
+ instanton tunnels to) and high-T phase (the phase that the instanton
2359
+ tunnels from).
2360
+ - *low_phase, high_phase* : identifier keys for the low-T and high-T
2361
+ phases.
2362
+ - *action* : The Euclidean action of the instanton.
2363
+ - *instanton* : Output from :func:`pathDeformation.fullTunneling`, or
2364
+ None for a second-order transition.
2365
+ - *trantype* : 1 or 2 for first or second-order transitions.
2366
+ """
2367
+ outdict = {} # keys are T values
2368
+ args = (start_phase, end_phase, V, dV, d2V, phitol, overlapAngle, nuclCriterion,
2369
+ fullTunneling_params, verbose, outdict, CF)
2370
+ Tmin = max(start_phase.Tmin, end_phase.Tmin)
2371
+
2372
+ # We want Tmax to be the highest temperature at which tunneling is theoretically
2373
+ # possible, i.e. where the potential difference between the two phases is at least
2374
+ # DVmin. Find the largest Tmax for which V(start_phase) > V(end_phase)
2375
+
2376
+ def DV(T):
2377
+ # Be very accurate about the minima!
2378
+ x0 = findLocalMinimum(start_phase.valAt(T), T, dV, d2V)
2379
+ x1 = findLocalMinimum(end_phase.valAt(T), T, dV, d2V)
2380
+ # This below is necessary because sometimes the phase tracing is not
2381
+ # accurate enough.
2382
+ if np.sqrt(np.sum((x0 - start_phase.valAt(T))**2)) > 1:
2383
+ x0 = start_phase.valAt(T)
2384
+ if np.sqrt(np.sum((x1 - end_phase.valAt(T))**2)) > 1:
2385
+ x1 = end_phase.valAt(T)
2386
+ x = x0 - x1
2387
+ dist = np.sqrt(x.dot(x))
2388
+ # Add small epsilon to pot. value in end phase to ensure breaking of degeneracy!
2389
+ # This might cause problems for very weak transitions,but on the otherhand, they
2390
+ # are not interesting
2391
+ eps = dist * 1e-3
2392
+ return V(x0, T) - (V(x1, T) + eps)
2393
+
2394
+ # Always check that the Tmax is where DV is > 0
2395
+ Tmax = findTunnelingTmax(DV, Tmin, end_phase.Tmax)
2396
+
2397
+ # Check for "hidden"" second order transitions
2398
+ if not checkNucleationPossibility(Tmin, Tmax, args, outdict, V, dV,
2399
+ start_phase, end_phase, CF,
2400
+ approximate_strength_threshold, verbose):
2401
+ rdict = dict(
2402
+ Tcrit=Tmax,
2403
+ Tnuc=Tmax,
2404
+ Tperc=Tmax,
2405
+ low_vev=end_phase.valAt(Tmax),
2406
+ high_vev=start_phase.valAt(Tmax),
2407
+ low_phase=end_phase.key,
2408
+ high_phase=start_phase.key,
2409
+ action=np.nan,
2410
+ full_tunneling_info=np.nan,
2411
+ instanton=np.nan,
2412
+ trantype=2,
2413
+ )
2414
+ rdict["Tcrit"] = rdict["Tnuc"] = Tmax
2415
+ return rdict
2416
+
2417
+ ncrit_Tmin = evalNucleationCriterion(Tmin, *args)
2418
+ ncrit_Tmax = evalNucleationCriterion(Tmax, *args)
2419
+
2420
+ # Is it a second order transition?
2421
+ # Does the nucleation happen already at Tmax?
2422
+ # This could be a second order transition or an
2423
+ # immediate transition after a another one
2424
+ if ncrit_Tmax >= 0:
2425
+ if verbose:
2426
+ print(f"Nucl. crit fullfilled at Tmax = {Tmax*CF}!")
2427
+ Tnuc = Tmax
2428
+ rdict = outdict[Tmax]
2429
+ rdict["Tcrit"] = Tmax
2430
+ if rdict["trantype"] != 2:
2431
+ filtered_outdict = {k: {kk: vv for kk, vv in v.items() if kk != "instanton"} for k, v in outdict.items()}
2432
+ filtered_outdict = dict(sorted(filtered_outdict.items()))
2433
+ rdict["full_tunneling_info"] = filtered_outdict
2434
+ # assert rdict['trantype'] == 2
2435
+ return rdict
2436
+
2437
+ # Is the nucleation criterion fulfilled at Tmin?
2438
+ if ncrit_Tmin < 0:
2439
+ if verbose:
2440
+ print("Nucleation criterion not fulfilled at Tmin")
2441
+ # The nucleation criterion is not fulfilled at the boundaries of
2442
+ # the temperature interval. It could be that the action is
2443
+ # U-shaped such that the nucleation criterion is not fulfilled
2444
+ # at Tmin and Tmax, but maybe somewhere in between.
2445
+ # In this case we need to find the minimum of the action
2446
+ # between Tmin and Tmax. This is done by minimizing the
2447
+ # tunneling action at T.
2448
+ if verbose:
2449
+ print("The bounce action might be U-shaped. Try to find the minimum of it")
2450
+ print("between Tmin = ", Tmin * CF, "GeV and Tmax = ", Tmax * CF, "GeV.")
2451
+ Tmin = findTminFromUShapedNucl(
2452
+ Tmin, Tmax, args, start_phase, end_phase, outdict, Ttol, maxiter, CF, verbose, nuclCriterion
2453
+ )
2454
+ # Hidden second order transition?
2455
+ if Tmin is None:
2456
+ rdict = dict(Tcrit=Tmax, Tnuc=Tmax, Tperc=Tmax,
2457
+ low_vev=end_phase.valAt(Tmax),
2458
+ high_vev=start_phase.valAt(Tmax),
2459
+ low_phase=end_phase.key,
2460
+ high_phase=start_phase.key,
2461
+ action=np.nan, full_tunneling_info=np.nan,
2462
+ instanton=np.nan, trantype=2)
2463
+ rdict["Tcrit"] = rdict["Tnuc"] = Tmax
2464
+ return rdict
2465
+
2466
+ # Check if the tunneling is energetically allowed between Tmin and Tmax
2467
+ try:
2468
+ Tnuc = optimize.brentq(evalNucleationCriterion, Tmin, Tmax, args=args, xtol=Ttol, maxiter=maxiter, disp=False)
2469
+ if verbose:
2470
+ print("Brentq done after first try. Tnuc = ", Tnuc * CF, "GeV")
2471
+ except ValueError:
2472
+ print("Brentq failed for first try, retrying with larger Ttol.")
2473
+ try:
2474
+ Tnuc = optimize.brentq(
2475
+ evalNucleationCriterion, Tmin, Tmax, args=args, xtol=Ttol * 10, maxiter=maxiter, disp=False
2476
+ )
2477
+ if verbose:
2478
+ print("Brentq done after second try! Tnuc = ", Tnuc * CF, "GeV")
2479
+ except Exception:
2480
+ if verbose:
2481
+ print("Brentq failed again after increasing the numerical tolerance by a factor 10.")
2482
+ raise errors.NucleationError("Nucleation criterion failed due to unknown reason.")
2483
+
2484
+ try:
2485
+ if Tnuc in outdict:
2486
+ rdict = outdict[Tnuc]
2487
+ # Remove 'instanton' key from each sub-dictionary in outdict before saving, because
2488
+ # it is no longer needed and could cause memory issues, becasuse it can become quite
2489
+ # large.
2490
+ filtered_outdict = {k: {kk: vv for kk, vv in v.items() if kk != "instanton"} for k, v in outdict.items()}
2491
+ # This is new and allows to re-use the computed tunneling solutions also at a
2492
+ # later part of the code, especially the percolation computation
2493
+ filtered_outdict = dict(sorted(filtered_outdict.items()))
2494
+ rdict["full_tunneling_info"] = filtered_outdict
2495
+ else:
2496
+ raise errors.NucleationError(
2497
+ "Tnuc not in outdict. This can only happen if the nucleation "
2498
+ "criterion could not be fulfilled at any temperature."
2499
+ )
2500
+ except UnboundLocalError as err:
2501
+ if verbose:
2502
+ print(
2503
+ "Tnuc was never defined. This happens if the nucleation "
2504
+ "criterion could not be fulfilled at any temperature, but a "
2505
+ "np.nan was returned. This is likely a numerical bug."
2506
+ )
2507
+ raise errors.NucleationError(
2508
+ "Nucleation criterion failed, probably due to a numerical error: "
2509
+ + str(err)
2510
+ )
2511
+
2512
+ if "trantype" in rdict and rdict["trantype"] > 0:
2513
+ if rdict["trantype"] == 2:
2514
+ # This is not a first-order transition, but actually a second-order one. This was
2515
+ # however only found here because the barrier was tiny and the nucleation
2516
+ # criterion is fulfilled trivially. We create a second-order transition dictionary
2517
+ # and return it.
2518
+ if verbose:
2519
+ print(
2520
+ "The transition is actually a second-order transition, "
2521
+ "because the action is zero at the transition."
2522
+ )
2523
+ rdict["Tcrit"] = rdict["Tnuc"] = Tnuc
2524
+ return rdict
2525
+ rdict["Tcrit"] = Tmax
2526
+ return rdict
2527
+ elif "trantype" in rdict and rdict["trantype"] == 0:
2528
+ # Check if there is a second-order transition just below the found Tnuc
2529
+ keys = sorted(key for key in outdict.keys() if key != "full_tunneling_info" and key < Tnuc)
2530
+ max_key = keys[-1] if keys else None
2531
+ if max_key is not None and outdict[max_key]["trantype"] == 2:
2532
+ # If there is a second-order transition just below the found Tnuc, return it
2533
+ if verbose:
2534
+ print(
2535
+ "The found transition had an infinite action. But found a "
2536
+ "second-order transition just below the found Tnuc. "
2537
+ "Returning it. Tcrit = ",
2538
+ max_key * CF,
2539
+ "GeV",
2540
+ )
2541
+ rdict = outdict[max_key]
2542
+ rdict["Tcrit"] = rdict["Tnuc"] = max_key
2543
+ return rdict
2544
+ else:
2545
+ if verbose:
2546
+ print(
2547
+ "The transition has action = 0 and trantype = 0, which means "
2548
+ "that it is a second-order transition, but no second-order "
2549
+ "transition was found just below the found Tnuc. This is "
2550
+ "likely a numerical issue."
2551
+ )
2552
+ return rdict
2553
+ else:
2554
+ raise errors.NucleationError(
2555
+ "The nucleation computation failed: Either the barrier is too high or the transition is second order."
2556
+ )
2557
+
2558
+
2559
+ def checkNucleationPossibility(Tmin, Tmax, args, outdict, V, dV, start_phase,
2560
+ end_phase, conversionFactor, apprx_strength_thr,
2561
+ verbose) -> tuple[bool, dict]:
2562
+ """Do some checks to see if tunneling is possible.
2563
+
2564
+ Parameters
2565
+ ----------
2566
+ tr : TransitionInfo
2567
+ The transition information
2568
+ Returns
2569
+ -------
2570
+ tuple[bool, dict] :
2571
+ (True, {}) if possible otherwise, (False, {returndict})."""
2572
+ # We want Tmax to be the highest temperature at which tunneling is theoretically
2573
+ # possible, i.e. where the potential difference between the two phases is at least
2574
+ # DVmin. Find the largest Tmax for which V(start_phase) > V(end_phase)
2575
+
2576
+ rdict = {}
2577
+ # Check 1 for "hidden" second-order transitions: action is zero at Tmax
2578
+ nuclCritTmax = evalNucleationCriterion(Tmax, *args)
2579
+ if np.isinf(nuclCritTmax):
2580
+ if Tmax in outdict:
2581
+ rdict = outdict[Tmax]
2582
+ if "trantype" in rdict and rdict["trantype"] == 2:
2583
+ if verbose:
2584
+ print(
2585
+ "The transition is actually a second-order transition "
2586
+ "because the action is zero at the transition."
2587
+ )
2588
+ return False
2589
+
2590
+ # Check 2 for "hidden" second-order transitions: the strength of the transition would in any case be tiny
2591
+ def approx_strength(T):
2592
+ # Approximate strength parameter of the transition, if it happened at temperature T
2593
+ DV = V(start_phase.valAt(T), T) - (V(end_phase.valAt(T), T))
2594
+ return np.abs(DV) / T**4
2595
+
2596
+ approx_strength_range = np.array([approx_strength(T) for T in np.linspace(Tmin, Tmax, 100)])
2597
+ if np.all(approx_strength_range < apprx_strength_thr):
2598
+ if verbose:
2599
+ print(f"PT strength {np.max(approx_strength_range)} below threshold: {apprx_strength_thr}.")
2600
+ print(f"This is treated as a second-order transition at Tmax = {Tmax * conversionFactor} GeV.")
2601
+ # Create a second-order transition dictionary
2602
+ return False
2603
+
2604
+ # Tunneling possible, return true and empty dict.
2605
+ return True
2606
+
2607
+
2608
+ def computeNucleationTemperature2(
2609
+ tr,
2610
+ V,
2611
+ dV,
2612
+ d2V,
2613
+ start_phase,
2614
+ end_phase,
2615
+ Tmax,
2616
+ conversionFactor,
2617
+ Ttol=1e-8,
2618
+ maxiter=100,
2619
+ phitol=1e-8,
2620
+ overlapAngle=45.0,
2621
+ nuclCriterion=lambda S, T: 140 - S / (T + 1e-100),
2622
+ approximate_strength_threshold=1e-4,
2623
+ verbose=False,
2624
+ fullTunneling_params={},
2625
+ ):
2626
+ """Refactored version of `computeNucleationTemperature`
2627
+ Find the instanton and nucleation temeprature for tunneling from
2628
+ `start_phase` to `end_phase`.
2629
+
2630
+ Parameters
2631
+ ----------
2632
+ tr: TransitionInfo
2633
+ Object containing the transition informations
2634
+ V, dV, d2V : callable
2635
+ The potential V(x,T) and its gradient.
2636
+ start_phase : PhaseInfo
2637
+ The metastable phase from which tunneling occurs.
2638
+ end_phase : PhaesInfo
2639
+ The destination phase.
2640
+ Tmax : float
2641
+ The highest temperature at which to try tunneling.
2642
+ Ttol : float, optional
2643
+ Tolerance for finding the nucleation temperature.
2644
+ maxiter : int, optional
2645
+ Maximum number of times to try tunneling.
2646
+ phitol : float, optional
2647
+ Tolerance for finding the minima.
2648
+ overlapAngle : float, optional
2649
+ If two phases are in the same direction, only try tunneling to the
2650
+ closer one. Set to zero to always try tunneling to all available phases.
2651
+ nuclCriterion : callable
2652
+ Function of the action *S* and temperature *T*. Should return 0 for the
2653
+ correct nucleation rate, > 0 for a low rate and < 0 for a high rate.
2654
+ Defaults to ``S/T - 140``.
2655
+ verbose : bool
2656
+ If true, print a message before each attempted tunneling.
2657
+ fullTunneling_params : dict
2658
+ Parameters to pass to :func:`pathDeformation.fullTunneling`.
2659
+
2660
+ Returns
2661
+ -------
2662
+ dict or None
2663
+ A description of the tunneling solution at the nucleation temperature,
2664
+ or None if there is no found solution. Has the following keys:
2665
+
2666
+ - *Tnuc* : the nucleation temperature
2667
+ - *low_vev, high_vev* : vevs for the low-T phase (the phase that the
2668
+ instanton tunnels to) and high-T phase (the phase that the instanton
2669
+ tunnels from).
2670
+ - *low_phase, high_phase* : identifier keys for the low-T and high-T
2671
+ phases.
2672
+ - *action* : The Euclidean action of the instanton.
2673
+ - *instanton* : Output from :func:`pathDeformation.fullTunneling`, or
2674
+ None for a second-order transition.
2675
+ - *trantype* : 1 or 2 for first or second-order transitions.
2676
+ """
2677
+ # If we need to calculate the bounce action brute-force if it is U-shaped, this is
2678
+ # the maximum number of test points to use.
2679
+ ntest = 100
2680
+ CF = conversionFactor
2681
+ outdict = {} # keys are T values
2682
+ # args for the tunneling function
2683
+ args = (start_phase, end_phase, V, dV, d2V, phitol, overlapAngle,
2684
+ nuclCriterion, fullTunneling_params, verbose, outdict, CF)
2685
+ # usually: lowest temperature of starting phase. But it can happen that for some
2686
+ # other reason, the start and end phases have been swapped. In that case, we don't
2687
+ # want to make an error here.
2688
+ Tmin = max(start_phase.Tmin, end_phase.Tmin)
2689
+ # usually highest temperature of end phase, unless it is below Tmin (which is bad)
2690
+ T_highest_other = max(Tmin, end_phase.Tmax)
2691
+ Tmax = min(Tmax, T_highest_other)
2692
+
2693
+ def DV(T):
2694
+ # Add small epsilon to pot. value in end phase to ensure breaking of degeneracy!
2695
+ x0 = start_phase.valAt(T)
2696
+ x1 = end_phase.valAt(T)
2697
+ dist = np.sqrt((x0-x1)**2)
2698
+ eps = dist * 1e-3
2699
+ return V(start_phase.valAt(T), T) - (V(end_phase.valAt(T), T) + eps)
2700
+
2701
+ # Check if the Potential difference is large enough to tunnel (numerically)
2702
+ Tmax = findTunnelingTmax(DV, Tmin, Tmax)
2703
+
2704
+ # check if tunneling is possible
2705
+ tPossible, rdict = checkNucleationPossibility(Tmin, Tmax, args, outdict, V, dV,
2706
+ start_phase, end_phase, CF,
2707
+ approximate_strength_threshold, verbose)
2708
+ if not tPossible:
2709
+ return rdict
2710
+
2711
+ ncrit_Tmin = evalNucleationCriterion(Tmin, *args)
2712
+ ncrit_Tmax = evalNucleationCriterion(Tmax, *args)
2713
+
2714
+ # Is it a second order transition?
2715
+ if ncrit_Tmax > 0:
2716
+ if verbose:
2717
+ print(f"Nucl. crit fullfilled at Tmax = {Tmax*CF}!")
2718
+ Tnuc = Tmax
2719
+ rdict = outdict[Tmax]
2720
+ assert rdict["trantype"] == 2
2721
+ return rdict
2722
+
2723
+ # Is there more than one bubble nucleated at Tmin
2724
+ # Or is the nucleation criterion u-shaped?
2725
+ if ncrit_Tmin < 0:
2726
+ # find a temperature where the the criterion becomes positive (> 1 bubble)
2727
+ def abort_fmin(T, outdict=outdict, nc=evalNucleationCriterion):
2728
+ val = nc(T, *args)
2729
+ if val > 0 and not np.isnan(val):
2730
+ raise StopIteration(T)
2731
+
2732
+ Tguess = np.nan
2733
+ # first search for a non-inf value of the nucleation criterion:
2734
+ for t in np.linspace(Tmin, Tmax, ntest, endpoint=False)[1:]:
2735
+ nc = evalNucleationCriterion(t, *args)
2736
+ if not np.isinf(nc):
2737
+ Tguess = t
2738
+ # Is the nucleation rate to low everywhere?
2739
+ if np.isnan(Tguess):
2740
+ if any([outdict[T]["trantype"] == 2 for T in outdict]):
2741
+ max_key = max(T for T in outdict if outdict[T]["trantype"] == 2)
2742
+ rdict = outdict[max_key]
2743
+ rdict["Tcrit"] = rdict["Tnuc"] = max_key
2744
+ return rdict
2745
+ if all([evalNucleationCriterion(T, *args) < 0 if T <= Tmax and T >= Tmin else True for T in outdict]):
2746
+ # This means the nucleation criterion is never fulfilled between Tmin and Tmax. The barrier is too high
2747
+ # to allow for the nucleation to be possible.
2748
+ raise errors.NucleationError("Nucleation rate too low at all temperatures.")
2749
+
2750
+ try:
2751
+ Tmin = abs(
2752
+ optimize.fmin(
2753
+ lambda x: -evalNucleationCriterion(abs(x), *args), Tguess, callback=abort_fmin, maxiter=20, disp=0
2754
+ )[0]
2755
+ )
2756
+ if evalNucleationCriterion(Tmin, *args) < 0:
2757
+ # The nucleation criterion is not fullfilled anywhere!
2758
+ raise errors.NucleationError("Nucleation rate to low at all temperatures!")
2759
+ except StopIteration as err:
2760
+ Tmin = abs(err.args[0])
2761
+
2762
+ try:
2763
+ Tnuc = optimize.brentq(evalNucleationCriterion, Tmin, Tmax, args=args, xtol=Ttol, maxiter=maxiter, disp=False)
2764
+ except Exception as e:
2765
+ print("Nucleation calculation failed!")
2766
+ print(e)
2767
+ raise errors.NucleationError("Unknown nulceation error!")
2768
+
2769
+ rdict = outdict[Tnuc]
2770
+ filtered_outdict = {k: {kk: vv for kk, vv in v.items() if kk != "instanton"} for k, v in outdict.items()}
2771
+ # This is new and allows to re-use the computed tunneling solutions also at a
2772
+ # later part of the code, especially the percolation computation
2773
+ filtered_outdict = dict(sorted(filtered_outdict.items()))
2774
+ rdict["full_tunneling_info"] = filtered_outdict
2775
+ return rdict
2776
+
2777
+
2778
+ def evalNucleationCriterion(T, start_phase, end_phase, V, dV,
2779
+ d2V, phitol, overlapAngle, nuclCriterion,
2780
+ fullTunneling_params, verbose, outdict,
2781
+ conversionFactor):
2782
+ """Find the lowest action tunneling solution.
2783
+
2784
+ Return ``nuclCriterion(S,T)``, and store a dictionary describing the
2785
+ transition in outdict for key `T`."""
2786
+ try:
2787
+ T = T[0] # need this when the function is run from optimize.fmin
2788
+ except Exception:
2789
+ pass
2790
+
2791
+ if T in outdict:
2792
+ return nuclCriterion(outdict[T]["action"], T, start_phase, end_phase)
2793
+
2794
+ # Loop through all the phases, adding acceptable minima
2795
+ x0 = findLocalMinimum(start_phase.valAt(T), T, dV, d2V)
2796
+ # Check if we accidentally hopped into the other minimum
2797
+ if np.sqrt(np.sum((x0 - start_phase.valAt(T))**2)) > 1:
2798
+ x0 = start_phase.valAt(T)
2799
+ V0 = V(x0, T)
2800
+ p = end_phase
2801
+ if p.Tmin > T:
2802
+ if verbose:
2803
+ print(
2804
+ "Phase %s not valid at T = %g" % (p.key, T * conversionFactor),
2805
+ "GeV < Tmin = ",
2806
+ p.Tmin * conversionFactor,
2807
+ "GeV",
2808
+ )
2809
+ # raise Exception("Tunneling not possible, low phase does not exist at T = ", T)
2810
+ return np.inf
2811
+
2812
+ if p.Tmax < T:
2813
+ if verbose:
2814
+ print(
2815
+ "Phase %s not valid at T = %g" % (p.key, T * conversionFactor),
2816
+ "GeV > Tmax = ",
2817
+ p.Tmax * conversionFactor,
2818
+ "GeV",
2819
+ )
2820
+ # raise Exception("Tunneling not possible, low phase does not exist at T = ", T)
2821
+ return -np.inf
2822
+
2823
+ x1 = findLocalMinimum(p.valAt(T), T, dV, d2V)
2824
+ if np.sqrt(np.sum((x1 - end_phase.valAt(T))**2)) > 1:
2825
+ x1 = end_phase.valAt(T)
2826
+ V1 = V(x1, T)
2827
+
2828
+ if V1 >= V0:
2829
+ if verbose:
2830
+ print(
2831
+ "Tunneling energetically not possible."
2832
+ + "Tmax might be too high or the numerical accuracy is not sufficient."
2833
+ + " This is not a problem, just a warning."
2834
+ )
2835
+
2836
+ tdict = dict(low_vev=x1, high_vev=x0, Tnuc=T, low_phase=end_phase.key, high_phase=start_phase.key)
2837
+ # tunnel_list.append(tdict)
2838
+ # This is a check to see if the two phases are in the same direction.
2839
+ # Future work, focusing on multiple discrete symmetries might
2840
+ # act here.
2841
+ # If two phases are in the same direction, we only want to try tunneling to the closer one:
2842
+ # if overlapAngle > 0:
2843
+ # excluded = []
2844
+ # cos_overlap = np.cos(overlapAngle * np.pi/180)
2845
+ # for i in range(1, len(tunnel_list)):
2846
+ # for j in range(i):
2847
+ # xi = tunnel_list[i]['low_vev']
2848
+ # xj = tunnel_list[j]['low_vev']
2849
+ # xi2 = np.sum((xi-x0)**2)
2850
+ # xj2 = np.sum((xj-x0)**2)
2851
+ # dotij = np.sum((xj-x0)*(xi-x0))
2852
+ # if dotij >= np.sqrt(xi2*xj2) * cos_overlap:
2853
+ # excluded.append(i if xi2 > xj2 else j)
2854
+ # for i in sorted(excluded)[::-1]:
2855
+ # del tunnel_list[i]
2856
+
2857
+ outdict = bounceAction(
2858
+ T, V, dV, outdict, tdict, verbose=verbose, conversionFactor=conversionFactor, **fullTunneling_params
2859
+ )
2860
+
2861
+ S = outdict[T]["action"]
2862
+ crit = nuclCriterion(S, T, start_phase, end_phase)
2863
+
2864
+ return crit
2865
+
2866
+
2867
+ def makeSecondOrderDict(transition, conversionFactor: float) -> dict:
2868
+ """Create a dictionary describing a second-order phase transition."""
2869
+ transition_dict = dict(
2870
+ Tcrit=transition.Tcrit,
2871
+ Tcrit_SM_GeV=transition.Tcrit * conversionFactor,
2872
+ Tnuc=transition.Tnuc,
2873
+ Tperc=transition.Tperc,
2874
+ low_vev=transition.low_vev,
2875
+ high_vev=transition.high_vev,
2876
+ low_phase=transition.low_phase,
2877
+ high_phase=transition.high_phase,
2878
+ action=np.nan,
2879
+ trantype=2,
2880
+ )
2881
+ return transition_dict
2882
+
2883
+
2884
+ def findLocalMinimum(
2885
+ X: np.ndarray, T: float, dV: callable, d2V: callable, phitol: float = 1e-6, step_size: float = 0.1
2886
+ ) -> np.ndarray:
2887
+ r"""Find the local minimum with the algorithm developed
2888
+ in BSMPT
2889
+
2890
+ .. math::
2891
+ \vec{\epsilon} = H^{-1}\nabla V
2892
+
2893
+ \vec{\phi}_{min}^{i+1} = \vec{\phi}_{min}^i - \vec{\epsilon}
2894
+
2895
+ Parameters
2896
+ ----------
2897
+ X : np.ndarray
2898
+ The initial guess for the location of the minimum
2899
+ T : float
2900
+ Temperature
2901
+ dV : callable
2902
+ Gradient of the potenital :math:`\nabla V(X, T)`
2903
+ d2V : callable
2904
+ Hessian matrix of the potenital :math:`\partial^2 V(X,T)/\partial \phi_i\partial\phi_j`
2905
+ phitol : float
2906
+ The desired accuracy of the minimum
2907
+ step_size : float
2908
+ The multiplyier controlling the step size in the direction of the minimum.
2909
+
2910
+ Returns
2911
+ -------
2912
+ np.ndarray:
2913
+ The minimum."""
2914
+ hess_offset = np.diag(np.ones_like(X) * 1e-5)
2915
+ niter = 0
2916
+ while True:
2917
+ niter += 1
2918
+ Hess = d2V(X, T)
2919
+ # Stop if zero eigenvalues
2920
+ if np.linalg.det(Hess) == 0:
2921
+ break
2922
+ Hinv = np.linalg.inv(Hess + hess_offset)
2923
+ eps = np.matmul(Hinv, dV(X, T))
2924
+ X = X - eps * step_size
2925
+ if np.sqrt(np.sum(eps * eps)) <= phitol:
2926
+ break
2927
+ if niter >= 200:
2928
+ break
2929
+ return X