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.
- transitionlistener/__init__.py +21 -0
- transitionlistener/bubbledynamics.py +2250 -0
- transitionlistener/bubbledynamics_fixedstep.py +2929 -0
- transitionlistener/check_installation.py +309 -0
- transitionlistener/ckm.py +91 -0
- transitionlistener/colors.py +288 -0
- transitionlistener/config.py +185 -0
- transitionlistener/constants.py +64 -0
- transitionlistener/counterterms/__init__.py +230 -0
- transitionlistener/counterterms/cw.py +298 -0
- transitionlistener/counterterms/tensors.py +343 -0
- transitionlistener/counterterms/twohdm.py +520 -0
- transitionlistener/counterterms/twohdm_solver.py +374 -0
- transitionlistener/errors.py +315 -0
- transitionlistener/examples/bp_et.yaml +64 -0
- transitionlistener/examples/bp_lisa.yaml +70 -0
- transitionlistener/examples/bp_muares.yaml +69 -0
- transitionlistener/examples/bp_pta.yaml +64 -0
- transitionlistener/examples/example_dark_photon.yaml +73 -0
- transitionlistener/examples/example_grid.yaml +55 -0
- transitionlistener/examples/example_line.yaml +51 -0
- transitionlistener/examples/example_point.yaml +73 -0
- transitionlistener/examples/example_random_conformal.yaml +42 -0
- transitionlistener/examples/example_ultranest.yaml +39 -0
- transitionlistener/finiteT.py +468 -0
- transitionlistener/generic_potential.py +1521 -0
- transitionlistener/gridplots.py +592 -0
- transitionlistener/gwfopt.py +604 -0
- transitionlistener/helper_functions.py +843 -0
- transitionlistener/hydrodynamics.py +647 -0
- transitionlistener/interface/__init__.py +95 -0
- transitionlistener/interface/check.py +51 -0
- transitionlistener/interface/cli.py +211 -0
- transitionlistener/interface/config_loader.py +264 -0
- transitionlistener/interface/logging_utils.py +30 -0
- transitionlistener/interface/output_schema.py +296 -0
- transitionlistener/interface/pipeline.py +300 -0
- transitionlistener/interface/samplers.py +565 -0
- transitionlistener/interface/scans.py +468 -0
- transitionlistener/interface/single_point.py +581 -0
- transitionlistener/interface/state.py +61 -0
- transitionlistener/lineplots.py +597 -0
- transitionlistener/logo/TL-logo_large.png +0 -0
- transitionlistener/logo/TL-logo_small.png +0 -0
- transitionlistener/models/TL_2HDM.py +512 -0
- transitionlistener/models/TL_conformal_dark_u1.py +202 -0
- transitionlistener/models/TL_dark_U1.py +198 -0
- transitionlistener/models/TL_dark_U1_g_parameterization.py +198 -0
- transitionlistener/models/TL_dark_flipflop.py +274 -0
- transitionlistener/models/templatePotential.py +260 -0
- transitionlistener/nucleation.py +412 -0
- transitionlistener/observability.py +790 -0
- transitionlistener/particles.py +436 -0
- transitionlistener/pathDeformation.py +1462 -0
- transitionlistener/percolation_adaptive_gridbuilders.py +1385 -0
- transitionlistener/percolation_adaptive_rate.py +1049 -0
- transitionlistener/percolation_adaptivestepsize.py +1632 -0
- transitionlistener/percolation_fixedstepsize.py +618 -0
- transitionlistener/phases.py +1499 -0
- transitionlistener/plot_settings.py +121 -0
- transitionlistener/plots.py +1781 -0
- transitionlistener/runtime_options.py +278 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/bandwidths.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/density.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/freqs.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log.txt +16 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rhogrid.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rholabels.txt +30 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/pulsar_list.txt +1 -0
- transitionlistener/tab_data/HLV-O2-PLI.csv +82 -0
- transitionlistener/tab_data/HLV-O2-noise.csv +83 -0
- transitionlistener/tab_data/HLVK-design-PLI.csv +61 -0
- transitionlistener/tab_data/HLVK-design-noise.csv +83 -0
- transitionlistener/tab_data/NG15_PLI_hc.csv +31 -0
- transitionlistener/tab_data/NG15_noise_hc.csv +56 -0
- transitionlistener/tab_data/PLI-dataset.csv +327 -0
- transitionlistener/tab_data/energydensities.dat +1001 -0
- transitionlistener/tab_data/finiteT_b.dat.txt +1999 -0
- transitionlistener/tab_data/finiteT_f.dat.txt +1000 -0
- transitionlistener/tab_data/geff_SM.dat +10009 -0
- transitionlistener/tab_data/muAres_f_Hz_PLI_h2OmegaGW.csv +131 -0
- transitionlistener/tab_data/muAres_f_Hz_h2Omega_noise.csv +99 -0
- transitionlistener/tab_data/noise-dataset.csv +327 -0
- transitionlistener/tab_data/numberdensities.dat +1001 -0
- transitionlistener/tab_data/pressures.dat +1001 -0
- transitionlistener/thermodynamics.py +686 -0
- transitionlistener/transitionObservables.py +1199 -0
- transitionlistener/transitionObservables_fixedstep.py +976 -0
- transitionlistener/transitions.py +1000 -0
- transitionlistener/tunneling1D.py +910 -0
- transitionlistener-2.0.0.dist-info/METADATA +817 -0
- transitionlistener-2.0.0.dist-info/RECORD +95 -0
- transitionlistener-2.0.0.dist-info/WHEEL +4 -0
- transitionlistener-2.0.0.dist-info/entry_points.txt +2 -0
- transitionlistener-2.0.0.dist-info/licenses/LICENSE.txt +674 -0
|
@@ -0,0 +1,2250 @@
|
|
|
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 copy
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
import math
|
|
18
|
+
import numpy as np
|
|
19
|
+
from scipy import optimize
|
|
20
|
+
from scipy import interpolate
|
|
21
|
+
from scipy import integrate
|
|
22
|
+
|
|
23
|
+
from transitionlistener import thermodynamics as td
|
|
24
|
+
from transitionlistener import constants as cn
|
|
25
|
+
from transitionlistener.helper_functions import derivative
|
|
26
|
+
from transitionlistener import errors
|
|
27
|
+
from transitionlistener.pathDeformation import bounceAction
|
|
28
|
+
from transitionlistener.finiteT import Jb_spline as Jb
|
|
29
|
+
from transitionlistener.finiteT import Jf_spline as Jf
|
|
30
|
+
from transitionlistener.nucleation import computeNucleationTemperature
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _maybe_dump_percolation_spline(state, stage: str, pot=None) -> None:
|
|
34
|
+
"""Dump the (T, P) spline of the current percolation state to disk.
|
|
35
|
+
|
|
36
|
+
Active only when the environment variable ``TL_PERC_DUMP_DIR`` is set.
|
|
37
|
+
Saves a linspace of ``TL_PERC_DUMP_N`` (default 10000) temperature points
|
|
38
|
+
and a monotone-preserving interpolation of the true-vacuum fraction
|
|
39
|
+
``P_t(T)`` as ``<stage>.npz`` inside the dump directory.
|
|
40
|
+
"""
|
|
41
|
+
import os
|
|
42
|
+
dump_dir = os.environ.get("TL_PERC_DUMP_DIR")
|
|
43
|
+
if not dump_dir or state is None:
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
n_support = int(os.environ.get("TL_PERC_DUMP_N", "10000"))
|
|
47
|
+
T = np.asarray(state.TSYM, dtype=float)
|
|
48
|
+
P = np.asarray(state.Pr, dtype=float)
|
|
49
|
+
finite = np.isfinite(T) & np.isfinite(P)
|
|
50
|
+
T = T[finite]
|
|
51
|
+
P = P[finite]
|
|
52
|
+
if T.size < 4:
|
|
53
|
+
return
|
|
54
|
+
order = np.argsort(T)
|
|
55
|
+
T = T[order]
|
|
56
|
+
P = P[order]
|
|
57
|
+
_, unique_idx = np.unique(T, return_index=True)
|
|
58
|
+
T = T[unique_idx]
|
|
59
|
+
P = P[unique_idx]
|
|
60
|
+
P = np.clip(P, 0.0, 1.0)
|
|
61
|
+
Pint = interpolate.PchipInterpolator(T, P, extrapolate=True)
|
|
62
|
+
T_grid = np.linspace(float(T.min()), float(T.max()), n_support)
|
|
63
|
+
P_grid = np.clip(Pint(T_grid), 0.0, 1.0)
|
|
64
|
+
os.makedirs(dump_dir, exist_ok=True)
|
|
65
|
+
extra = {}
|
|
66
|
+
if pot is not None:
|
|
67
|
+
if hasattr(pot, "conversionFactor"):
|
|
68
|
+
extra["CF"] = float(pot.conversionFactor)
|
|
69
|
+
if hasattr(pot, "g"):
|
|
70
|
+
extra["g"] = float(pot.g)
|
|
71
|
+
if hasattr(pot, "l"):
|
|
72
|
+
extra["lambda"] = float(pot.l)
|
|
73
|
+
np.savez(os.path.join(dump_dir, f"{stage}.npz"),
|
|
74
|
+
T=T_grid, P=P_grid, T_support=T, P_support=P, **extra)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class PercolationGrid:
|
|
81
|
+
"""Initial temperature grid and traced phase-overlap bounds for percolation."""
|
|
82
|
+
|
|
83
|
+
TSYM: np.ndarray
|
|
84
|
+
Tstart: float
|
|
85
|
+
TpercApprox: float
|
|
86
|
+
tmin: float
|
|
87
|
+
tmax: float
|
|
88
|
+
TBROmin: float
|
|
89
|
+
TBROmax: float
|
|
90
|
+
free_support_bank: np.ndarray | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class PercolationState:
|
|
95
|
+
"""Current percolation profile plus support-bank diagnostics."""
|
|
96
|
+
|
|
97
|
+
TSYM: np.ndarray
|
|
98
|
+
Sr: np.ndarray
|
|
99
|
+
Hr: np.ndarray
|
|
100
|
+
Pr: np.ndarray
|
|
101
|
+
Tb: np.ndarray
|
|
102
|
+
explored_tmin: float | None = None
|
|
103
|
+
rebuild_count: int = 0
|
|
104
|
+
support_bank: np.ndarray | None = None
|
|
105
|
+
free_support_bank: np.ndarray | None = None
|
|
106
|
+
action_temperatures: np.ndarray | None = None
|
|
107
|
+
cold_tail_last_temperature: float | None = None
|
|
108
|
+
cold_tail_last_probability: float | None = None
|
|
109
|
+
cold_tail_last_integral: float | None = None
|
|
110
|
+
cold_tail_last_log_gamma_h4: float | None = None
|
|
111
|
+
cold_tail_post_peak_streak: int = 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _finalize_percolation_state(
|
|
115
|
+
state: PercolationState,
|
|
116
|
+
outdict: dict,
|
|
117
|
+
action_temperatures_before: np.ndarray,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Freeze the support-bank bookkeeping after a percolation solve.
|
|
120
|
+
|
|
121
|
+
The adaptive solver stores both temperatures already sampled by the
|
|
122
|
+
percolation ODE and additional temperatures at which the tunneling action
|
|
123
|
+
had to be reevaluated. This helper normalises both sets onto sorted,
|
|
124
|
+
duplicate-free temperature grids so the diagnostics layer can report them
|
|
125
|
+
consistently.
|
|
126
|
+
"""
|
|
127
|
+
state.support_bank = _temperature_grid(state.support_bank if state.support_bank is not None else state.TSYM)
|
|
128
|
+
if state.free_support_bank is not None:
|
|
129
|
+
state.action_temperatures = _temperature_grid(
|
|
130
|
+
np.concatenate((state.support_bank, np.asarray(state.free_support_bank, dtype=float)))
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
previous = _temperature_grid(action_temperatures_before)
|
|
135
|
+
current = _temperature_grid(_action_outdict_temperatures(outdict))
|
|
136
|
+
if previous.size == 0:
|
|
137
|
+
state.action_temperatures = current
|
|
138
|
+
return
|
|
139
|
+
state.action_temperatures = np.asarray(
|
|
140
|
+
[
|
|
141
|
+
float(value)
|
|
142
|
+
for value in current
|
|
143
|
+
if not np.any(
|
|
144
|
+
np.isclose(
|
|
145
|
+
previous,
|
|
146
|
+
float(value),
|
|
147
|
+
atol=1e-10 * max(np.max(np.abs(previous)), abs(float(value)), 1.0),
|
|
148
|
+
rtol=0.0,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
],
|
|
152
|
+
dtype=float,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def calcPercAndEvolve(
|
|
157
|
+
outdict: dict,
|
|
158
|
+
Tnuc: float | None,
|
|
159
|
+
phase_symmetric,
|
|
160
|
+
phase_broken,
|
|
161
|
+
pot,
|
|
162
|
+
vw=1.0,
|
|
163
|
+
rtol=1e-4,
|
|
164
|
+
nAction=50,
|
|
165
|
+
verbose=False,
|
|
166
|
+
return_metadata: bool = False,
|
|
167
|
+
):
|
|
168
|
+
"""Compute the percolation profile with the configured percolation algorithm.
|
|
169
|
+
|
|
170
|
+
The public return shape is kept for compatibility:
|
|
171
|
+
``Tperc, TSYM, H, P, Tb, S`` plus optional diagnostics metadata.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
CF = pot.conversionFactor
|
|
175
|
+
action_temperatures_before = _action_outdict_temperatures(outdict)
|
|
176
|
+
settings = _build_percolation_settings(pot, int(nAction))
|
|
177
|
+
state: PercolationState | None = None
|
|
178
|
+
try:
|
|
179
|
+
Tstart = None if Tnuc is None else float(Tnuc)
|
|
180
|
+
if Tstart is not None and not np.isfinite(Tstart):
|
|
181
|
+
Tstart = None
|
|
182
|
+
overlap_tmin, overlap_tmax = _phase_overlap_interval(
|
|
183
|
+
pot,
|
|
184
|
+
phase_symmetric,
|
|
185
|
+
phase_broken,
|
|
186
|
+
)
|
|
187
|
+
from transitionlistener.percolation_adaptivestepsize import _initial_temperature_grid
|
|
188
|
+
|
|
189
|
+
if settings.algorithm_mode == "adaptive_step_size":
|
|
190
|
+
# The adaptive step size solver keeps the same physical pass structure:
|
|
191
|
+
# coarse approximate Tperc, then a step-2 solve with P = 0 in the
|
|
192
|
+
# Hubble rate, then a step-3 solve with the P-dependent Hubble
|
|
193
|
+
# rate. Only the support-point management changes, which now lives
|
|
194
|
+
# in the dedicated adaptive step size module.
|
|
195
|
+
from transitionlistener.percolation_adaptivestepsize import (
|
|
196
|
+
_initial_percolation_scan_dynamiczoomwindow,
|
|
197
|
+
_refine_percolation_temperature_dynamiczoomwindow,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
grid = _initial_temperature_grid(
|
|
201
|
+
outdict,
|
|
202
|
+
pot,
|
|
203
|
+
Tstart,
|
|
204
|
+
phase_symmetric,
|
|
205
|
+
phase_broken,
|
|
206
|
+
vw,
|
|
207
|
+
int(nAction),
|
|
208
|
+
settings,
|
|
209
|
+
overlap_tmin,
|
|
210
|
+
overlap_tmax,
|
|
211
|
+
verbose,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
state = _initial_percolation_scan_dynamiczoomwindow(
|
|
215
|
+
grid,
|
|
216
|
+
settings,
|
|
217
|
+
outdict,
|
|
218
|
+
pot,
|
|
219
|
+
phase_symmetric,
|
|
220
|
+
phase_broken,
|
|
221
|
+
CF,
|
|
222
|
+
vw,
|
|
223
|
+
verbose,
|
|
224
|
+
)
|
|
225
|
+
_maybe_dump_percolation_spline(state, "step2", pot)
|
|
226
|
+
Tperc_prev = _solve_for_initial_Tperc(state, settings, pot, verbose)
|
|
227
|
+
Tperc, state = _refine_percolation_temperature_dynamiczoomwindow(
|
|
228
|
+
state,
|
|
229
|
+
grid,
|
|
230
|
+
settings,
|
|
231
|
+
outdict,
|
|
232
|
+
pot,
|
|
233
|
+
phase_symmetric,
|
|
234
|
+
phase_broken,
|
|
235
|
+
CF,
|
|
236
|
+
vw,
|
|
237
|
+
Tperc_prev,
|
|
238
|
+
rtol,
|
|
239
|
+
verbose,
|
|
240
|
+
)
|
|
241
|
+
_maybe_dump_percolation_spline(state, "step3", pot)
|
|
242
|
+
else:
|
|
243
|
+
from transitionlistener.percolation_fixedstepsize import (
|
|
244
|
+
_initial_percolation_scan,
|
|
245
|
+
_refine_percolation_temperature,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
grid = _initial_temperature_grid(
|
|
249
|
+
outdict,
|
|
250
|
+
pot,
|
|
251
|
+
Tstart,
|
|
252
|
+
phase_symmetric,
|
|
253
|
+
phase_broken,
|
|
254
|
+
vw,
|
|
255
|
+
int(nAction),
|
|
256
|
+
settings,
|
|
257
|
+
overlap_tmin,
|
|
258
|
+
overlap_tmax,
|
|
259
|
+
verbose,
|
|
260
|
+
)
|
|
261
|
+
state = _initial_percolation_scan(
|
|
262
|
+
grid,
|
|
263
|
+
settings,
|
|
264
|
+
outdict,
|
|
265
|
+
pot,
|
|
266
|
+
phase_symmetric,
|
|
267
|
+
phase_broken,
|
|
268
|
+
CF,
|
|
269
|
+
vw,
|
|
270
|
+
verbose,
|
|
271
|
+
)
|
|
272
|
+
_maybe_dump_percolation_spline(state, "step2", pot)
|
|
273
|
+
Tperc_prev = _solve_for_initial_Tperc(state, settings, pot, verbose)
|
|
274
|
+
Tperc, state = _refine_percolation_temperature(
|
|
275
|
+
state,
|
|
276
|
+
grid,
|
|
277
|
+
settings,
|
|
278
|
+
outdict,
|
|
279
|
+
pot,
|
|
280
|
+
phase_symmetric,
|
|
281
|
+
phase_broken,
|
|
282
|
+
CF,
|
|
283
|
+
vw,
|
|
284
|
+
Tperc_prev,
|
|
285
|
+
rtol,
|
|
286
|
+
verbose,
|
|
287
|
+
)
|
|
288
|
+
_maybe_dump_percolation_spline(state, "step3", pot)
|
|
289
|
+
_finalize_percolation_state(state, outdict, action_temperatures_before)
|
|
290
|
+
jitter_diagnostic = _raise_if_action_rate_jitter_unresolved(
|
|
291
|
+
state,
|
|
292
|
+
settings,
|
|
293
|
+
CF,
|
|
294
|
+
pot=pot,
|
|
295
|
+
phase_symmetric=phase_symmetric,
|
|
296
|
+
phase_broken=phase_broken,
|
|
297
|
+
outdict=outdict,
|
|
298
|
+
)
|
|
299
|
+
tnuc_estimate = estimate_spline_tnuc_from_rate_history(
|
|
300
|
+
state.TSYM,
|
|
301
|
+
state.Hr,
|
|
302
|
+
state.Sr,
|
|
303
|
+
state.Pr,
|
|
304
|
+
vw=vw,
|
|
305
|
+
)
|
|
306
|
+
metadata = PercolationDiagnostics.from_state(state, grid, jitter_diagnostic, tnuc_estimate)
|
|
307
|
+
if return_metadata:
|
|
308
|
+
return Tperc, state.TSYM, state.Hr, state.Pr, state.Tb, state.Sr, metadata
|
|
309
|
+
return Tperc, state.TSYM, state.Hr, state.Pr, state.Tb, state.Sr
|
|
310
|
+
except Exception as err:
|
|
311
|
+
if state is not None:
|
|
312
|
+
_finalize_percolation_state(state, outdict, action_temperatures_before)
|
|
313
|
+
try:
|
|
314
|
+
setattr(err, "percolation_state", state)
|
|
315
|
+
setattr(err, "percolation_rebuild_count", int(state.rebuild_count))
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
raise err
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _build_percolation_settings(pot, nAction: int):
|
|
322
|
+
"""Validate ``pot.config.percolationConf`` and attach runtime-only values."""
|
|
323
|
+
|
|
324
|
+
conf = pot.config.percolationConf
|
|
325
|
+
static_n_action = max(int(nAction), 1)
|
|
326
|
+
algorithm_mode = str(getattr(conf, "algorithm_mode", "adaptive_step_size"))
|
|
327
|
+
if algorithm_mode not in {"adaptive_step_size", "fixed_step_size"}:
|
|
328
|
+
raise errors.PercolationError(
|
|
329
|
+
"percolation_algorithm_mode must be 'adaptive_step_size' or "
|
|
330
|
+
f"'fixed_step_size', got {algorithm_mode!r}."
|
|
331
|
+
)
|
|
332
|
+
integral_method = str(getattr(conf, "integral_method", "ode"))
|
|
333
|
+
if integral_method not in {"ode", "double_integral"}:
|
|
334
|
+
raise errors.PercolationError(
|
|
335
|
+
"percolation_integral_method must be 'ode' or "
|
|
336
|
+
f"'double_integral', got {integral_method!r}."
|
|
337
|
+
)
|
|
338
|
+
time_temperature_mode = str(getattr(conf, "time_temperature_mode", "sound_speed"))
|
|
339
|
+
if time_temperature_mode not in {"sound_speed", "bag"}:
|
|
340
|
+
raise errors.PercolationError(
|
|
341
|
+
"percolation_time_temperature_mode must be 'sound_speed' or "
|
|
342
|
+
f"'bag', got {time_temperature_mode!r}."
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
n_action_min = max(int(getattr(conf, "n_action_min", 20)), 2)
|
|
346
|
+
n_action_increment = max(int(getattr(conf, "n_action_increment", 5)), 1)
|
|
347
|
+
n_action_max = max(int(getattr(conf, "n_action_max", 50)), n_action_min)
|
|
348
|
+
boundary_basis = n_action_min if algorithm_mode == "adaptive_step_size" else static_n_action
|
|
349
|
+
|
|
350
|
+
conf.algorithm_mode = algorithm_mode
|
|
351
|
+
conf.integral_method = integral_method
|
|
352
|
+
conf.time_temperature_mode = time_temperature_mode
|
|
353
|
+
conf.n_action_min = n_action_min
|
|
354
|
+
conf.n_action_increment = n_action_increment
|
|
355
|
+
conf.n_action_max = n_action_max
|
|
356
|
+
conf.max_action_temperatures = max(int(getattr(conf, "max_action_temperatures", 100)), 1)
|
|
357
|
+
conf.max_boundary_n = int(conf.max_boundary_ratio * boundary_basis)
|
|
358
|
+
conf.large_delta_p_refine_threshold = float(getattr(conf, "large_delta_p_refine_threshold", 0.1))
|
|
359
|
+
conf.large_delta_p_success_threshold = float(getattr(conf, "large_delta_p_success_threshold", 0.2))
|
|
360
|
+
conf.jitter_GH4_threshold = max(float(getattr(conf, "jitter_GH4_threshold", 1.0)), 0.0)
|
|
361
|
+
conf.jitter_rescue = bool(getattr(conf, "jitter_rescue", False))
|
|
362
|
+
conf.n_jitter_save = max(int(getattr(conf, "n_jitter_save", 20)), 0)
|
|
363
|
+
conf.acc_tperc = float(getattr(conf, "acc_tperc", 1e-2))
|
|
364
|
+
conf.acc_tfinal = float(getattr(conf, "acc_tfinal", 1e-2))
|
|
365
|
+
conf.acc_rh = float(getattr(conf, "acc_rh", 1e-2))
|
|
366
|
+
return conf
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _phase_overlap_interval(
|
|
370
|
+
pot,
|
|
371
|
+
phase_symmetric,
|
|
372
|
+
phase_broken,
|
|
373
|
+
Tnuc: float | None = None,
|
|
374
|
+
) -> tuple[float, float]:
|
|
375
|
+
"""Return the common traced temperature interval of both phases."""
|
|
376
|
+
|
|
377
|
+
tmin = max(float(phase_symmetric.Tmin), float(phase_broken.Tmin))
|
|
378
|
+
tmax = min(float(phase_symmetric.Tmax), float(phase_broken.Tmax))
|
|
379
|
+
if not np.isfinite(tmin) or not np.isfinite(tmax) or tmin >= tmax:
|
|
380
|
+
raise errors.PercolationError(
|
|
381
|
+
"No overlapping temperature interval between traced phases: "
|
|
382
|
+
f"Tsym in [{phase_symmetric.Tmin:.8g}, {phase_symmetric.Tmax:.8g}], "
|
|
383
|
+
f"Tbro in [{phase_broken.Tmin:.8g}, {phase_broken.Tmax:.8g}]."
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if Tnuc is not None:
|
|
387
|
+
tracing_conf = getattr(getattr(pot, "config", None), "tracingConf", None)
|
|
388
|
+
nuc_tol = float(getattr(tracing_conf, "nucleation_Ttol", 1e-8))
|
|
389
|
+
scale = max(abs(tmin), abs(tmax), abs(float(Tnuc)), 1.0)
|
|
390
|
+
temp_tol = max(nuc_tol, 1e-10 * scale)
|
|
391
|
+
if Tnuc < tmin - temp_tol or Tnuc > tmax + temp_tol:
|
|
392
|
+
raise errors.PercolationError(
|
|
393
|
+
"Nucleation temperature is outside the traced phase-overlap interval: "
|
|
394
|
+
f"Tnuc={Tnuc:.8g}, overlap=[{tmin:.8g}, {tmax:.8g}], tol={temp_tol:.3g}. "
|
|
395
|
+
"This indicates inconsistent phase tracing / tunnelling data."
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return tmin, tmax
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _temperature_grid(
|
|
402
|
+
values: np.ndarray | list[float] | tuple[float, ...] | None,
|
|
403
|
+
) -> np.ndarray:
|
|
404
|
+
"""Return finite temperatures sorted descending with near-duplicates removed."""
|
|
405
|
+
|
|
406
|
+
if values is None:
|
|
407
|
+
return np.asarray([], dtype=float)
|
|
408
|
+
raw = np.asarray(values, dtype=float).reshape(-1)
|
|
409
|
+
raw = raw[np.isfinite(raw)]
|
|
410
|
+
if raw.size == 0:
|
|
411
|
+
return np.asarray([], dtype=float)
|
|
412
|
+
|
|
413
|
+
ordered = np.sort(raw)[::-1]
|
|
414
|
+
unique = [float(ordered[0])]
|
|
415
|
+
for candidate in ordered[1:]:
|
|
416
|
+
candidate = float(candidate)
|
|
417
|
+
scale = max(abs(unique[-1]), abs(candidate), 1.0)
|
|
418
|
+
if not math.isclose(unique[-1], candidate, abs_tol=1e-10 * scale, rel_tol=0.0):
|
|
419
|
+
unique.append(candidate)
|
|
420
|
+
return np.asarray(unique, dtype=float)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def g_eff_DS(T_DS: float, pot, phase) -> float:
|
|
424
|
+
"""Calculate the effective energy degrees of freedom in the dark sector.
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
T : float
|
|
429
|
+
Temperature at which to evaluate g_eff
|
|
430
|
+
pot : generic_potential
|
|
431
|
+
The effetive potential object
|
|
432
|
+
phase : PhaseInfo
|
|
433
|
+
The phase of the system, either symmetric or broken.
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
----------
|
|
437
|
+
float
|
|
438
|
+
Effective degrees of freedom in the dark sector."""
|
|
439
|
+
try:
|
|
440
|
+
vevT = phase.valAt(T_DS)
|
|
441
|
+
except BaseException:
|
|
442
|
+
print("Warning: TBRO is too low for interpolation of vev, using T = 0 value")
|
|
443
|
+
# Temperature is to low for interpolation of vev, use T = 0 value
|
|
444
|
+
vevT = pot.X0
|
|
445
|
+
bosons = pot.boson_massSq(vevT, 0)*(~pot.mass_spectrum.is_SM_bosons)
|
|
446
|
+
fermions = pot.fermion_massSq(vevT)*(~pot.mass_spectrum.is_SM_fermions)
|
|
447
|
+
geff = td.e_geffDS(bosons, fermions, T_DS)
|
|
448
|
+
return geff
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def h_eff_DS(T_DS: float, pot, phase) -> float:
|
|
452
|
+
"""Calculate the effective entropy degrees of freedom in the dark sector.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
T : float
|
|
457
|
+
Temperature at which to evaluate g_eff
|
|
458
|
+
pot : generic_potential
|
|
459
|
+
The effetive potential object
|
|
460
|
+
phase : PhaseInfo
|
|
461
|
+
The phase of the system, either symmetric or broken.
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
----------
|
|
465
|
+
geff : float
|
|
466
|
+
Effective degrees of freedom in the dark sector."""
|
|
467
|
+
try:
|
|
468
|
+
vevT = phase.valAt(T_DS)
|
|
469
|
+
except BaseException:
|
|
470
|
+
print("Warning: TBRO is too low for interpolation of vev, using T = 0 value")
|
|
471
|
+
# Temperature is to low for interpolation of vev, use T = 0 value
|
|
472
|
+
vevT = pot.X0
|
|
473
|
+
|
|
474
|
+
# Set SM masses to zero:
|
|
475
|
+
bosons = pot.boson_massSq(vevT, 0)*(~pot.mass_spectrum.is_SM_bosons)
|
|
476
|
+
fermions = pot.fermion_massSq(vevT)*(~pot.mass_spectrum.is_SM_fermions)
|
|
477
|
+
geff = td.s_geffDS(bosons, fermions, T_DS)
|
|
478
|
+
return geff
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def energyDensity(pot, phase, T: float | np.ndarray, include_decoupled=True) -> float | np.ndarray:
|
|
482
|
+
r"""This function calls the implementation in the effective potential.
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
pot : generic_potential
|
|
487
|
+
Effective potential
|
|
488
|
+
phase : PhaseInfo
|
|
489
|
+
The phase information
|
|
490
|
+
T : float|np.ndarray
|
|
491
|
+
The temperature at which to compute the energy density
|
|
492
|
+
include_decoupled : bool, optional
|
|
493
|
+
If true, also account for the energy density in a decoupled sector
|
|
494
|
+
|
|
495
|
+
Returns
|
|
496
|
+
-------
|
|
497
|
+
float|np.ndarray :
|
|
498
|
+
The energy density at `T`."""
|
|
499
|
+
X = phase.valAt(T)
|
|
500
|
+
return pot.energyDensity(X, T, include_decoupled=include_decoupled)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def Gamma(T: float | np.ndarray, S: float | np.ndarray) -> np.ndarray:
|
|
504
|
+
"""Calculate the bubble nucleation rate.
|
|
505
|
+
|
|
506
|
+
Parameters
|
|
507
|
+
----------
|
|
508
|
+
T : float | np.ndarray
|
|
509
|
+
Symmetric phase temperature
|
|
510
|
+
S : float | np.ndarray
|
|
511
|
+
The action at temperature T
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
----------
|
|
515
|
+
Gamma : np.ndarray
|
|
516
|
+
The bubble nucleation rate."""
|
|
517
|
+
S = np.atleast_1d(S)
|
|
518
|
+
T = np.atleast_1d(T)
|
|
519
|
+
result = np.zeros_like(T, dtype=float)
|
|
520
|
+
|
|
521
|
+
mask_zero = S == 0
|
|
522
|
+
mask_inf = np.isinf(S)
|
|
523
|
+
mask_valid = ~(mask_zero | mask_inf)
|
|
524
|
+
|
|
525
|
+
result[mask_zero] = np.inf
|
|
526
|
+
result[mask_inf] = np.nan
|
|
527
|
+
# Ignore RuntimeWarnings in sqrt and exp (e.g., for invalid or overflow values)
|
|
528
|
+
with np.errstate(invalid="ignore", over="ignore"):
|
|
529
|
+
result[mask_valid] = (
|
|
530
|
+
T[mask_valid] ** 4
|
|
531
|
+
* np.sqrt(S[mask_valid] / (2 * np.pi * T[mask_valid])) ** 3
|
|
532
|
+
* np.exp(-S[mask_valid] / T[mask_valid])
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
return result
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def logGamma(T: float | np.ndarray, S: float | np.ndarray) -> np.ndarray:
|
|
539
|
+
"""Calculate the log10 of the bubble nucleation rate.
|
|
540
|
+
|
|
541
|
+
Parameters
|
|
542
|
+
----------
|
|
543
|
+
T : float | np.ndarray
|
|
544
|
+
Symmetric phase temperature
|
|
545
|
+
S : float | np.ndarray
|
|
546
|
+
The action at temperature T
|
|
547
|
+
|
|
548
|
+
Returns
|
|
549
|
+
----------
|
|
550
|
+
Gamma : np.ndarray
|
|
551
|
+
The bubble nucleation rate."""
|
|
552
|
+
S = np.atleast_1d(S)
|
|
553
|
+
T = np.atleast_1d(T)
|
|
554
|
+
result = np.zeros_like(T, dtype=float)
|
|
555
|
+
|
|
556
|
+
mask_zero = S == 0
|
|
557
|
+
mask_inf = np.isinf(S)
|
|
558
|
+
mask_valid = ~(mask_zero | mask_inf)
|
|
559
|
+
|
|
560
|
+
result[mask_zero] = np.inf
|
|
561
|
+
result[mask_inf] = np.nan
|
|
562
|
+
# Ignore RuntimeWarnings in sqrt and exp (e.g., for invalid or overflow values)
|
|
563
|
+
with np.errstate(invalid="ignore", over="ignore"):
|
|
564
|
+
result[mask_valid] = (
|
|
565
|
+
4 * np.log(T[mask_valid])
|
|
566
|
+
+ 3 / 2 * np.log(S[mask_valid] / (2 * np.pi * T[mask_valid]))
|
|
567
|
+
- S[mask_valid] / T[mask_valid]
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
return result
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def HubbleParameter(rho: float | np.ndarray, CF: float) -> float:
|
|
574
|
+
"""Hubble parameter.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
rho : float
|
|
579
|
+
The energy density of the unverse.
|
|
580
|
+
CF : float
|
|
581
|
+
Conversion factor to convert internal units to GeV.
|
|
582
|
+
Returns
|
|
583
|
+
-------
|
|
584
|
+
float :
|
|
585
|
+
The Hubble parameter."""
|
|
586
|
+
return np.sqrt(8 * np.pi / 3 * rho) / (cn.Mpl_GeV / CF)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def calcSoundSpeedSq(pot, X, T) -> float:
|
|
590
|
+
"""Compute the symmetric-phase sound speed squared at ``(X, T)``.
|
|
591
|
+
|
|
592
|
+
Decoupled radiation is excluded because it does not participate in the
|
|
593
|
+
local time-temperature relation of the transitioning plasma.
|
|
594
|
+
"""
|
|
595
|
+
T_abs = abs(float(T))
|
|
596
|
+
dT = max(float(getattr(pot, "T_eps", 1.0e-3)), T_abs * 1.0e-4)
|
|
597
|
+
if T_abs > 0.0:
|
|
598
|
+
dT = min(dT, 0.25 * T_abs)
|
|
599
|
+
dVdT = pot.dVdT(X, T, dT=dT, include_decoupled=False)
|
|
600
|
+
d2VdT2 = pot.d2VdT2(X, T, dT=dT, include_decoupled=False)
|
|
601
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
602
|
+
cs_sq = np.asarray(dVdT, dtype=float) / (float(T) * np.asarray(d2VdT2, dtype=float))
|
|
603
|
+
return float(np.squeeze(cs_sq))
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _time_temperature_factors(
|
|
607
|
+
pot,
|
|
608
|
+
phase,
|
|
609
|
+
T: np.ndarray,
|
|
610
|
+
mode: str | None = None,
|
|
611
|
+
) -> tuple[np.ndarray | None, np.ndarray | None]:
|
|
612
|
+
"""Return ``(c_s^2, a/a_hot)`` arrays for the generalized integral.
|
|
613
|
+
|
|
614
|
+
``None, None`` is returned for the bag-limit mode so the historical ODE is
|
|
615
|
+
used without any extra thermodynamic finite-difference cost.
|
|
616
|
+
"""
|
|
617
|
+
mode = "sound_speed" if mode is None else str(mode)
|
|
618
|
+
if mode not in {"sound_speed", "bag"}:
|
|
619
|
+
raise errors.PercolationError(
|
|
620
|
+
f"Unknown percolation time-temperature mode {mode!r}. "
|
|
621
|
+
"Supported modes are 'sound_speed' and 'bag'."
|
|
622
|
+
)
|
|
623
|
+
if mode == "bag" or pot is None or phase is None:
|
|
624
|
+
return None, None
|
|
625
|
+
|
|
626
|
+
temperatures = np.asarray(T, dtype=float)
|
|
627
|
+
sound_speed_sq = np.full_like(temperatures, 1.0 / 3.0, dtype=float)
|
|
628
|
+
for i, temp in enumerate(temperatures):
|
|
629
|
+
try:
|
|
630
|
+
cs_sq = calcSoundSpeedSq(pot, phase.valAt(float(temp)), float(temp))
|
|
631
|
+
except Exception:
|
|
632
|
+
cs_sq = np.nan
|
|
633
|
+
if np.isfinite(cs_sq) and cs_sq > 0.0:
|
|
634
|
+
sound_speed_sq[i] = float(cs_sq)
|
|
635
|
+
|
|
636
|
+
# d ln a / dT = -1 / (3 c_s^2 T), normalized to a(T_hot)=1.
|
|
637
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
638
|
+
integrand = -1.0 / (3.0 * sound_speed_sq * temperatures)
|
|
639
|
+
integrand = np.nan_to_num(integrand, nan=0.0, posinf=0.0, neginf=0.0)
|
|
640
|
+
ln_a = integrate.cumulative_trapezoid(integrand, x=temperatures, initial=0.0)
|
|
641
|
+
ln_a = np.clip(ln_a, -700.0, 700.0)
|
|
642
|
+
scale_factor = np.exp(ln_a)
|
|
643
|
+
return sound_speed_sq, scale_factor
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def calcAction(pot, T: float, start_phase, end_phase, outdict: dict, verbose: bool = False,
|
|
647
|
+
phitol: float = 1e-6) -> float:
|
|
648
|
+
"""Calculate the action at temperature `T`
|
|
649
|
+
|
|
650
|
+
Parameters
|
|
651
|
+
----------
|
|
652
|
+
pot : generic_potential
|
|
653
|
+
The effective potential object
|
|
654
|
+
T : float
|
|
655
|
+
The temperature
|
|
656
|
+
start_phase : PhaseInfo
|
|
657
|
+
The information about the high temperature phase
|
|
658
|
+
end_phase : PhaseInfo
|
|
659
|
+
The information about the low temperature phase
|
|
660
|
+
outdict : dict
|
|
661
|
+
The dictionary storing the action evaluations with the key `T`.
|
|
662
|
+
verbose : bool, optional
|
|
663
|
+
Set the output level
|
|
664
|
+
phitol : float, optional
|
|
665
|
+
Set the accuracy of the minimisation of the potential minima.
|
|
666
|
+
|
|
667
|
+
Returns
|
|
668
|
+
-------
|
|
669
|
+
float :
|
|
670
|
+
The action at temperature `T`."""
|
|
671
|
+
|
|
672
|
+
if T in outdict:
|
|
673
|
+
return outdict[T]["action"]
|
|
674
|
+
|
|
675
|
+
from transitionlistener.phases import findLocalMinimum
|
|
676
|
+
|
|
677
|
+
x0 = findLocalMinimum(start_phase.valAt(T), T, pot.gradV, pot.d2V)
|
|
678
|
+
x1 = findLocalMinimum(end_phase.valAt(T), T, pot.gradV, pot.d2V)
|
|
679
|
+
tdict = dict(low_vev=x1, high_vev=x0)
|
|
680
|
+
|
|
681
|
+
outdict = bounceAction(
|
|
682
|
+
T, pot.Vtot, pot.gradV, outdict, tdict, verbose, pot.conversionFactor,
|
|
683
|
+
**pot.config.tracingConf.tunneling_params)
|
|
684
|
+
return outdict[T]["action"]
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def percIntegral(
|
|
688
|
+
T: np.ndarray,
|
|
689
|
+
H: np.ndarray,
|
|
690
|
+
S: np.ndarray,
|
|
691
|
+
vw=1.0,
|
|
692
|
+
*,
|
|
693
|
+
entropy_density: np.ndarray | None = None,
|
|
694
|
+
cooling_factor: np.ndarray | None = None,
|
|
695
|
+
) -> float:
|
|
696
|
+
"""Perform the percolation integral beteen Tstart and Tend.
|
|
697
|
+
|
|
698
|
+
Parameters
|
|
699
|
+
----------
|
|
700
|
+
T : np.ndarray
|
|
701
|
+
Temperature of the dark sector symmetric phase. It must start with the
|
|
702
|
+
nucleation temperature and go down to the percolation temp.
|
|
703
|
+
H : np.ndarray
|
|
704
|
+
Hubble rate evaluated at T
|
|
705
|
+
S : np.ndarray
|
|
706
|
+
S3 Euclidian bounce action evaluated at T
|
|
707
|
+
vw : float
|
|
708
|
+
|
|
709
|
+
Returns
|
|
710
|
+
-------
|
|
711
|
+
float
|
|
712
|
+
The integral evaluated at the last temperature ``T[-1]``.
|
|
713
|
+
"""
|
|
714
|
+
# See eq. (4.57) in 2305.02357
|
|
715
|
+
# The ordering of the array is important
|
|
716
|
+
if len(T) > 1:
|
|
717
|
+
if not T[0] >= T[1]:
|
|
718
|
+
raise errors.PercolationError("T is not decreasing in the percolation integral.")
|
|
719
|
+
|
|
720
|
+
T = np.asarray(T, dtype=float)
|
|
721
|
+
H = np.asarray(H, dtype=float)
|
|
722
|
+
S = np.asarray(S, dtype=float)
|
|
723
|
+
vol_int = np.array([integrate.trapezoid(1 / H[i:], x=T[i:]) for i in range(len(T))])
|
|
724
|
+
with np.errstate(invalid="ignore"):
|
|
725
|
+
integrant = Gamma(T, S) / T**4 / H * vol_int**3
|
|
726
|
+
y = integrate.trapezoid(np.nan_to_num(integrant), x=T)
|
|
727
|
+
return 4 * np.pi / 3 * vw**3 * y
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
# ---------------------------------------------------------------------------
|
|
731
|
+
# ODE-based percolation integral
|
|
732
|
+
# ---------------------------------------------------------------------------
|
|
733
|
+
#
|
|
734
|
+
# The standard ``percIntegral`` computes I(T) via a nested double integral
|
|
735
|
+
# at O(N^2) per evaluation (O(N^3) for a full sweep over the grid). The
|
|
736
|
+
# functions below reformulate I(T) as a 4-component ODE chain that can be
|
|
737
|
+
# integrated in a single O(N) sweep from T_hot to T_cold:
|
|
738
|
+
#
|
|
739
|
+
# A(T) = Gamma(T) / (T^4 * H(T)) (nucleation-rate density)
|
|
740
|
+
# J_0' = -A J_0(T_hot) = 0
|
|
741
|
+
# J_1' = -J_0 / H J_1(T_hot) = 0
|
|
742
|
+
# J_2' = -2 J_1 / H J_2(T_hot) = 0
|
|
743
|
+
# J_3' = -3 J_2 / H J_3(T_hot) = 0
|
|
744
|
+
#
|
|
745
|
+
# I(T) = (4 pi / 3) * v_w^3 * J_3(T)
|
|
746
|
+
# P(T) = 1 - exp(-I(T))
|
|
747
|
+
#
|
|
748
|
+
# Here ' = d/dT (with T *decreasing* toward the cold boundary).
|
|
749
|
+
# See the derivation in the module-level docstring of
|
|
750
|
+
# ``percolation_adaptivestepsize.py``.
|
|
751
|
+
# ---------------------------------------------------------------------------
|
|
752
|
+
|
|
753
|
+
_LOG_GAMMA_SOURCE_FLOOR = -700.0 # exp(-700) ~ 1e-304, safely above denormal threshold
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _log_bag_gamma_source_array(
|
|
757
|
+
T: np.ndarray,
|
|
758
|
+
S: np.ndarray,
|
|
759
|
+
H: np.ndarray,
|
|
760
|
+
) -> np.ndarray:
|
|
761
|
+
r"""Compute the bag-limit ``ln gamma(T)`` source term element-wise.
|
|
762
|
+
|
|
763
|
+
Works entirely in log-space so the exponentially suppressed hot tail
|
|
764
|
+
never underflows.
|
|
765
|
+
|
|
766
|
+
Parameters
|
|
767
|
+
----------
|
|
768
|
+
T, S, H : np.ndarray
|
|
769
|
+
Temperature, bounce action, and Hubble rate arrays (internal units,
|
|
770
|
+
same length, T decreasing).
|
|
771
|
+
|
|
772
|
+
Returns
|
|
773
|
+
-------
|
|
774
|
+
np.ndarray
|
|
775
|
+
``ln gamma`` at each grid point. Entries where the action is zero or
|
|
776
|
+
infinite are set to ``-inf``.
|
|
777
|
+
"""
|
|
778
|
+
T = np.asarray(T, dtype=float)
|
|
779
|
+
S = np.asarray(S, dtype=float)
|
|
780
|
+
H = np.asarray(H, dtype=float)
|
|
781
|
+
result = np.full_like(T, -np.inf)
|
|
782
|
+
valid = np.isfinite(S) & (S > 0.0) & np.isfinite(H) & (H > 0.0) & (T > 0.0)
|
|
783
|
+
if not np.any(valid):
|
|
784
|
+
return result
|
|
785
|
+
# ln(Gamma) = 4 ln T + 3/2 ln(S/(2 pi T)) - S/T
|
|
786
|
+
# Bag-limit gamma = Gamma / (H T^4), so the explicit 4 ln(T)
|
|
787
|
+
# prefactor in Gamma cancels.
|
|
788
|
+
Tv = T[valid]
|
|
789
|
+
Sv = S[valid]
|
|
790
|
+
Hv = H[valid]
|
|
791
|
+
result[valid] = (
|
|
792
|
+
1.5 * np.log(Sv / (2.0 * np.pi * Tv))
|
|
793
|
+
- Sv / Tv
|
|
794
|
+
- np.log(Hv)
|
|
795
|
+
)
|
|
796
|
+
return result
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def percIntegralODE(
|
|
800
|
+
T: np.ndarray,
|
|
801
|
+
H: np.ndarray,
|
|
802
|
+
S: np.ndarray,
|
|
803
|
+
vw: float = 1.0,
|
|
804
|
+
*,
|
|
805
|
+
sound_speed_sq: np.ndarray | None = None,
|
|
806
|
+
scale_factor: np.ndarray | None = None,
|
|
807
|
+
) -> np.ndarray:
|
|
808
|
+
r"""Compute I(T_i) at every grid point via the J_n ODE chain.
|
|
809
|
+
|
|
810
|
+
With ``sound_speed_sq`` and ``scale_factor`` this solves the generalized
|
|
811
|
+
EOS form using
|
|
812
|
+
``gamma = Gamma a^3 / (H T 3 c_s^2)`` and
|
|
813
|
+
``nu = 1 / (H T 3 c_s^2 a)``. Without those arrays it falls back to the
|
|
814
|
+
historical bag-limit relation, where ``c_s^2 = 1/3`` and ``a(T)`` cancels
|
|
815
|
+
to the simpler transport factor ``1/H``.
|
|
816
|
+
|
|
817
|
+
Parameters
|
|
818
|
+
----------
|
|
819
|
+
T : np.ndarray
|
|
820
|
+
Temperatures in descending order (T[0] = hottest).
|
|
821
|
+
H : np.ndarray
|
|
822
|
+
Hubble rate at each temperature.
|
|
823
|
+
S : np.ndarray
|
|
824
|
+
Bounce action S_3 at each temperature.
|
|
825
|
+
vw : float
|
|
826
|
+
Bubble-wall velocity.
|
|
827
|
+
|
|
828
|
+
Returns
|
|
829
|
+
-------
|
|
830
|
+
np.ndarray
|
|
831
|
+
``I(T_i)`` at each grid point (same length as *T*).
|
|
832
|
+
``P(T_i) = 1 - exp(-I(T_i))``.
|
|
833
|
+
"""
|
|
834
|
+
T = np.asarray(T, dtype=float)
|
|
835
|
+
H = np.asarray(H, dtype=float)
|
|
836
|
+
S = np.asarray(S, dtype=float)
|
|
837
|
+
N = T.size
|
|
838
|
+
if N == 0:
|
|
839
|
+
return np.zeros(0)
|
|
840
|
+
if N == 1:
|
|
841
|
+
return np.zeros(1)
|
|
842
|
+
if T[0] < T[-1]:
|
|
843
|
+
raise errors.PercolationError(
|
|
844
|
+
"T is not decreasing in percIntegralODE."
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
use_general_eos = sound_speed_sq is not None and scale_factor is not None
|
|
848
|
+
if use_general_eos:
|
|
849
|
+
sound_speed_sq = np.asarray(sound_speed_sq, dtype=float)
|
|
850
|
+
scale_factor = np.asarray(scale_factor, dtype=float)
|
|
851
|
+
if sound_speed_sq.shape != T.shape or scale_factor.shape != T.shape:
|
|
852
|
+
raise errors.PercolationError(
|
|
853
|
+
"sound_speed_sq and scale_factor must have the same shape as T."
|
|
854
|
+
)
|
|
855
|
+
valid = (
|
|
856
|
+
np.isfinite(S)
|
|
857
|
+
& (S > 0.0)
|
|
858
|
+
& np.isfinite(H)
|
|
859
|
+
& (H > 0.0)
|
|
860
|
+
& np.isfinite(sound_speed_sq)
|
|
861
|
+
& (sound_speed_sq > 0.0)
|
|
862
|
+
& np.isfinite(scale_factor)
|
|
863
|
+
& (scale_factor > 0.0)
|
|
864
|
+
& (T > 0.0)
|
|
865
|
+
)
|
|
866
|
+
log_gamma_source = np.full_like(T, -np.inf)
|
|
867
|
+
if np.any(valid):
|
|
868
|
+
log_gamma_source[valid] = (
|
|
869
|
+
np.asarray(logGamma(T[valid], S[valid]), dtype=float)
|
|
870
|
+
+ 3.0 * np.log(scale_factor[valid])
|
|
871
|
+
- np.log(H[valid] * T[valid] * 3.0 * sound_speed_sq[valid])
|
|
872
|
+
)
|
|
873
|
+
log_transport = np.full_like(T, -np.inf)
|
|
874
|
+
log_transport[valid] = -np.log(H[valid] * T[valid] * 3.0 * sound_speed_sq[valid] * scale_factor[valid])
|
|
875
|
+
else:
|
|
876
|
+
# Bag-limit source and transport factors.
|
|
877
|
+
log_gamma_source = _log_bag_gamma_source_array(T, S, H)
|
|
878
|
+
inv_H = np.where(np.isfinite(H) & (H > 0.0), 1.0 / H, 0.0)
|
|
879
|
+
log_transport = np.full_like(T, -np.inf)
|
|
880
|
+
valid_h = inv_H > 0.0
|
|
881
|
+
log_transport[valid_h] = np.log(inv_H[valid_h])
|
|
882
|
+
|
|
883
|
+
# PCHIP requires finite values: clamp -inf to a floor so the source term
|
|
884
|
+
# evaluates to zero in the RHS without upsetting the interpolant.
|
|
885
|
+
log_gamma_source = np.maximum(log_gamma_source, _LOG_GAMMA_SOURCE_FLOOR)
|
|
886
|
+
log_transport = np.maximum(log_transport, _LOG_GAMMA_SOURCE_FLOOR)
|
|
887
|
+
|
|
888
|
+
# --- Trim clamped hot-end outliers ----------------------------------------
|
|
889
|
+
# The support bank occasionally places one or a few temperatures far above
|
|
890
|
+
# the nucleation peak (e.g. T[0] = 70 GeV while T[1] = 0.8 GeV). Those
|
|
891
|
+
# hot-end points have a floored source term (no contribution to the
|
|
892
|
+
# integral), but their presence creates a huge interval in u = ln(T) space
|
|
893
|
+
# (Δu ~ 4.4 vs. Δu ~ 0.01 everywhere else). The PCHIP interpolant across
|
|
894
|
+
# such a stretched interval produces a large slope that causes DOP853's
|
|
895
|
+
# initial step-size estimate to collapse to sub-machine-epsilon values.
|
|
896
|
+
#
|
|
897
|
+
# Because gamma ≈ 0 in the clamped region, J_n(T_first_non_clamped) = 0 is a
|
|
898
|
+
# valid initial condition — trimming these points changes I(T_cold) by at
|
|
899
|
+
# most exp(_LOG_GAMMA_SOURCE_FLOOR) * ΔT ≈ 0.
|
|
900
|
+
#
|
|
901
|
+
# The caller expects len(I_values) == len(T_input), so we restore the
|
|
902
|
+
# trimmed hot-end positions with I = 0 before returning.
|
|
903
|
+
_FLOOR_MARGIN = 1.0
|
|
904
|
+
n_trim = int(np.argmax(log_gamma_source > _LOG_GAMMA_SOURCE_FLOOR + _FLOOR_MARGIN))
|
|
905
|
+
if n_trim > 0 and n_trim < N - 1:
|
|
906
|
+
T = T[n_trim:]
|
|
907
|
+
log_gamma_source = log_gamma_source[n_trim:]
|
|
908
|
+
log_transport = log_transport[n_trim:]
|
|
909
|
+
N = T.size
|
|
910
|
+
else:
|
|
911
|
+
n_trim = 0
|
|
912
|
+
# --------------------------------------------------------------------------
|
|
913
|
+
|
|
914
|
+
# Build PCHIP interpolants in log(T) space.
|
|
915
|
+
#
|
|
916
|
+
# The temperatures are log-spaced, so u = ln(T) is *linearly* spaced —
|
|
917
|
+
# the PCHIP nodes are equidistant in u, which gives much better
|
|
918
|
+
# conditioning than T-space where the hot tail is enormously stretched.
|
|
919
|
+
# More importantly, solving the ODE in u-space avoids the "Required step
|
|
920
|
+
# size is less than spacing between numbers" failure that DOP853 can hit
|
|
921
|
+
# when T is very small (~1e-5 GeV): in T-space the minimum allowed step
|
|
922
|
+
# size is |h| > eps * |T| ~ 2e-21, whereas in u-space the span is
|
|
923
|
+
# O(10) and machine precision is never a bottleneck.
|
|
924
|
+
T_asc = T[::-1].copy()
|
|
925
|
+
u_asc = np.log(T_asc) # ascending (T_asc is ascending → u_asc ascending)
|
|
926
|
+
log_gamma_source_asc = log_gamma_source[::-1].copy()
|
|
927
|
+
log_transport_asc = log_transport[::-1].copy()
|
|
928
|
+
|
|
929
|
+
log_gamma_source_interp = interpolate.PchipInterpolator(u_asc, log_gamma_source_asc, extrapolate=True)
|
|
930
|
+
log_transport_interp = interpolate.PchipInterpolator(u_asc, log_transport_asc, extrapolate=True)
|
|
931
|
+
|
|
932
|
+
# The ODE chain in u = ln(T) coordinates. Chain rule: d/du = T · d/dT,
|
|
933
|
+
# so each RHS term acquires a factor of T = exp(u):
|
|
934
|
+
#
|
|
935
|
+
# dJ0/du = -gamma(T) · T
|
|
936
|
+
# dJ1/du = -J0 · nu(T) · T
|
|
937
|
+
# dJ2/du = -2 J1 · nu(T) · T
|
|
938
|
+
# dJ3/du = -3 J2 · nu(T) · T
|
|
939
|
+
#
|
|
940
|
+
# We integrate from u_hot = ln(T[0]) downward to u_cold = ln(T[-1]).
|
|
941
|
+
# solve_ivp supports a decreasing t_span, so we pass (u_hot, u_cold)
|
|
942
|
+
# and it steps in the negative-u direction.
|
|
943
|
+
#
|
|
944
|
+
# Absolute tolerance: J0 starts at values as small as exp(-S_3/T_hot)
|
|
945
|
+
# which can be ~exp(-800) ~ 1e-348. atol=1e-300 keeps the solver from
|
|
946
|
+
# treating J0 as zero in the early (very suppressed) hot-tail phase.
|
|
947
|
+
u_eval = np.log(T) # decreasing (mirrors T which is decreasing)
|
|
948
|
+
|
|
949
|
+
def rhs(u, y):
|
|
950
|
+
t = np.exp(u)
|
|
951
|
+
log_gamma_source_value = float(log_gamma_source_interp(u))
|
|
952
|
+
gamma_source = (
|
|
953
|
+
np.exp(log_gamma_source_value)
|
|
954
|
+
if log_gamma_source_value > _LOG_GAMMA_SOURCE_FLOOR
|
|
955
|
+
else 0.0
|
|
956
|
+
)
|
|
957
|
+
lt = float(log_transport_interp(u))
|
|
958
|
+
transport = np.exp(lt) if lt > _LOG_GAMMA_SOURCE_FLOOR else 0.0
|
|
959
|
+
J0, J1, J2, J3 = y
|
|
960
|
+
return [
|
|
961
|
+
-gamma_source * t, # dJ0/du
|
|
962
|
+
-J0 * transport * t, # dJ1/du
|
|
963
|
+
-2.0 * J1 * transport * t, # dJ2/du
|
|
964
|
+
-3.0 * J2 * transport * t, # dJ3/du
|
|
965
|
+
]
|
|
966
|
+
|
|
967
|
+
sol = integrate.solve_ivp(
|
|
968
|
+
rhs,
|
|
969
|
+
t_span=(float(u_eval[0]), float(u_eval[-1])),
|
|
970
|
+
y0=[0.0, 0.0, 0.0, 0.0],
|
|
971
|
+
method="DOP853",
|
|
972
|
+
t_eval=u_eval,
|
|
973
|
+
rtol=1e-10,
|
|
974
|
+
atol=1e-300,
|
|
975
|
+
dense_output=False,
|
|
976
|
+
)
|
|
977
|
+
if sol.status != 0:
|
|
978
|
+
raise errors.PercolationError(
|
|
979
|
+
"percIntegralODE: the percolation integral solver could not reach "
|
|
980
|
+
"the cold end of the temperature grid. The action S_3(T) is most "
|
|
981
|
+
"likely non-smooth on the scale of the support points (numerical "
|
|
982
|
+
"jitter from path deformation), so the adaptive ODE step shrinks "
|
|
983
|
+
"below floating-point spacing and the solver gives up. "
|
|
984
|
+
"Suggested fixes: (a) rerun with precision_mode: tunneltight or "
|
|
985
|
+
"benchmark to tighten the bounce-action path deformation; "
|
|
986
|
+
"(b) probe the action with tests/probe_bounce_stability.py to "
|
|
987
|
+
"localise the jittery T-range; "
|
|
988
|
+
"(c) if the model is multi-field, lower precision_deform_fRatioConv "
|
|
989
|
+
"(default 1e-2 for Ndim>=2) further. "
|
|
990
|
+
f"Underlying solver message: {sol.message}"
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
J3 = sol.y[3] # J3 at each T_i
|
|
994
|
+
I_trimmed = (4.0 * np.pi / 3.0) * vw**3 * J3
|
|
995
|
+
# Restore trimmed hot-end points with I = 0 (A ≈ 0 there).
|
|
996
|
+
if n_trim > 0:
|
|
997
|
+
I_values = np.concatenate([np.zeros(n_trim), I_trimmed])
|
|
998
|
+
else:
|
|
999
|
+
I_values = I_trimmed
|
|
1000
|
+
return np.asarray(I_values, dtype=float)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def percIntegralODE_full_sweep(
|
|
1004
|
+
T: np.ndarray,
|
|
1005
|
+
H: np.ndarray,
|
|
1006
|
+
S: np.ndarray,
|
|
1007
|
+
vw: float = 1.0,
|
|
1008
|
+
*,
|
|
1009
|
+
pot=None,
|
|
1010
|
+
phase_symmetric=None,
|
|
1011
|
+
time_temperature_mode: str | None = None,
|
|
1012
|
+
integral_method: str | None = None,
|
|
1013
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
1014
|
+
r"""One-shot ODE sweep: compute both I(T_i) and P(T_i) at every grid point.
|
|
1015
|
+
|
|
1016
|
+
This replaces the per-point ``percIntegral`` calls in the step-2 / step-3
|
|
1017
|
+
inner loops.
|
|
1018
|
+
|
|
1019
|
+
Parameters
|
|
1020
|
+
----------
|
|
1021
|
+
T, H, S : np.ndarray
|
|
1022
|
+
Temperatures (descending), Hubble rates, bounce actions.
|
|
1023
|
+
vw : float
|
|
1024
|
+
Bubble-wall velocity.
|
|
1025
|
+
|
|
1026
|
+
Returns
|
|
1027
|
+
-------
|
|
1028
|
+
I_values : np.ndarray
|
|
1029
|
+
Percolation integral ``I(T_i)`` at each grid point.
|
|
1030
|
+
P_values : np.ndarray
|
|
1031
|
+
True-vacuum fraction ``P(T_i) = 1 - exp(-I(T_i))``.
|
|
1032
|
+
"""
|
|
1033
|
+
method = "ode" if integral_method is None else str(integral_method)
|
|
1034
|
+
if method == "double_integral":
|
|
1035
|
+
I_values = np.asarray(
|
|
1036
|
+
[percIntegral(T[: i + 1], H[: i + 1], S[: i + 1], vw=vw) for i in range(len(T))],
|
|
1037
|
+
dtype=float,
|
|
1038
|
+
)
|
|
1039
|
+
elif method == "ode":
|
|
1040
|
+
sound_speed_sq, scale_factor = _time_temperature_factors(
|
|
1041
|
+
pot,
|
|
1042
|
+
phase_symmetric,
|
|
1043
|
+
np.asarray(T, dtype=float),
|
|
1044
|
+
time_temperature_mode,
|
|
1045
|
+
)
|
|
1046
|
+
I_values = percIntegralODE(
|
|
1047
|
+
T,
|
|
1048
|
+
H,
|
|
1049
|
+
S,
|
|
1050
|
+
vw=vw,
|
|
1051
|
+
sound_speed_sq=sound_speed_sq,
|
|
1052
|
+
scale_factor=scale_factor,
|
|
1053
|
+
)
|
|
1054
|
+
else:
|
|
1055
|
+
raise errors.PercolationError(
|
|
1056
|
+
f"Unknown percolation integral method {integral_method!r}. "
|
|
1057
|
+
"Supported methods are 'ode' and 'double_integral'."
|
|
1058
|
+
)
|
|
1059
|
+
P_values = 1.0 - np.exp(-I_values)
|
|
1060
|
+
return I_values, P_values
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def approxNucleationCriterion(T_DS: float, S: float, pot, phase_sym, phase_bro) -> float:
|
|
1064
|
+
r"""
|
|
1065
|
+
Calculate the nucleation criterion for a given temperature and action.
|
|
1066
|
+
This function computes the nucleation criterion based on the following equation:
|
|
1067
|
+
|
|
1068
|
+
.. math::
|
|
1069
|
+
|
|
1070
|
+
\frac{\Gamma}{H^4} = 1
|
|
1071
|
+
|
|
1072
|
+
where :math:`\Gamma` denotes the bubble nucleation rate and :math:`H`
|
|
1073
|
+
is the Hubble parameter during radiation domination.
|
|
1074
|
+
|
|
1075
|
+
Parameters
|
|
1076
|
+
----------
|
|
1077
|
+
T_DS : float
|
|
1078
|
+
Temperature of the DS.
|
|
1079
|
+
S : float
|
|
1080
|
+
Action at temperature TDS
|
|
1081
|
+
pot : generic_potential
|
|
1082
|
+
The effective potential object
|
|
1083
|
+
phase_sym : PhaseInfo
|
|
1084
|
+
The information about the high temperature phase
|
|
1085
|
+
phase_bro : PhaseInfo
|
|
1086
|
+
The information about the low temperature phase
|
|
1087
|
+
|
|
1088
|
+
Returns
|
|
1089
|
+
-------
|
|
1090
|
+
float
|
|
1091
|
+
Result of the nucleation criterion, 0 if fulfilled.
|
|
1092
|
+
"""
|
|
1093
|
+
T_DS += 1e-100
|
|
1094
|
+
|
|
1095
|
+
if S == -np.inf:
|
|
1096
|
+
return np.inf
|
|
1097
|
+
elif S == np.inf:
|
|
1098
|
+
return -np.inf
|
|
1099
|
+
# This approximation identifies the PT-sector temperature with T_DS and
|
|
1100
|
+
# keeps any hidden-sector temperature ratio temperature independent.
|
|
1101
|
+
T = T_DS
|
|
1102
|
+
|
|
1103
|
+
rho = energyDensity(pot, phase_sym, T)
|
|
1104
|
+
H = HubbleParameter(rho, pot.conversionFactor)
|
|
1105
|
+
logG = logGamma(T_DS, S)[0] # Returnvalue is an array
|
|
1106
|
+
crit = logG - 4 * np.log(H)
|
|
1107
|
+
# crit is > 0 if more than one bubble is nucleated until T
|
|
1108
|
+
# crit is < 0 if less than one bubble is nucleated until T
|
|
1109
|
+
return crit
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _approx_percolation_criterion(
|
|
1113
|
+
T: float, outdict: dict, pot, phase_sym, phase_bro, Tmax: float, Tmin: float, vw: float, verbose: bool = False
|
|
1114
|
+
) -> float:
|
|
1115
|
+
"""Alternative method to estimate the percolation criterion. This uses a
|
|
1116
|
+
smaller dT for the derivatives and computes betaH from the action not the
|
|
1117
|
+
nucleation rate.
|
|
1118
|
+
|
|
1119
|
+
Parameters
|
|
1120
|
+
----------
|
|
1121
|
+
T : float
|
|
1122
|
+
The temperature.
|
|
1123
|
+
outdict : dict
|
|
1124
|
+
Dictionary storing the action evaluation
|
|
1125
|
+
pot : generic_potential
|
|
1126
|
+
The effective potential object
|
|
1127
|
+
phase_sym : PhaseInfo
|
|
1128
|
+
The high temperature phase information
|
|
1129
|
+
phase_bro : PhaseInfo
|
|
1130
|
+
The low temperature phase information
|
|
1131
|
+
Tmax : float
|
|
1132
|
+
The maximal temperature, where to compute the nucleation criterion (usually Tnuc)
|
|
1133
|
+
Tmin : float
|
|
1134
|
+
The minimal temperature where to compute the nucleation criterion, usually
|
|
1135
|
+
the minimal temp. of coexistence of the phases.
|
|
1136
|
+
vw : float
|
|
1137
|
+
The bubble wall velocity.
|
|
1138
|
+
verbose : bool, optional
|
|
1139
|
+
Set the output level
|
|
1140
|
+
|
|
1141
|
+
Returns
|
|
1142
|
+
----------
|
|
1143
|
+
float :
|
|
1144
|
+
0 if the criterion is met for T, < 0 if T is to large, > 0 if T is to small."""
|
|
1145
|
+
try:
|
|
1146
|
+
T = T[0] # need this when the function is run from optimize.newton, because outdict needs a hashable key
|
|
1147
|
+
except Exception:
|
|
1148
|
+
pass
|
|
1149
|
+
|
|
1150
|
+
if T >= Tmax:
|
|
1151
|
+
return -np.inf
|
|
1152
|
+
if T <= Tmin:
|
|
1153
|
+
return np.inf
|
|
1154
|
+
|
|
1155
|
+
dT = T * 1e-3
|
|
1156
|
+
if T + dT > Tmax:
|
|
1157
|
+
dT = (Tmax - T) / 100.0
|
|
1158
|
+
|
|
1159
|
+
if T + dT >= Tmax:
|
|
1160
|
+
return -np.inf
|
|
1161
|
+
if T + dT <= Tmin:
|
|
1162
|
+
return np.inf
|
|
1163
|
+
|
|
1164
|
+
f_perc = float(pot.config.percolationConf.f_perc)
|
|
1165
|
+
Iperc = -np.log1p(-f_perc)
|
|
1166
|
+
SdT = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
|
|
1167
|
+
S = calcAction(pot, T, phase_sym, phase_bro, outdict)
|
|
1168
|
+
if np.isinf(SdT) or np.isinf(S):
|
|
1169
|
+
return np.inf
|
|
1170
|
+
|
|
1171
|
+
if S == 0 or SdT == 0:
|
|
1172
|
+
return -np.inf
|
|
1173
|
+
|
|
1174
|
+
betaH = (SdT - S) / dT - S / T
|
|
1175
|
+
rho = energyDensity(pot, phase_sym, T)
|
|
1176
|
+
H = HubbleParameter(rho, pot.conversionFactor)
|
|
1177
|
+
G = Gamma(T, S)
|
|
1178
|
+
if not np.isfinite(betaH) or abs(betaH) < 1e-30:
|
|
1179
|
+
if verbose:
|
|
1180
|
+
print("Warning: betaH is not finite or zero in percolation approximation. betaH =", betaH)
|
|
1181
|
+
return np.inf
|
|
1182
|
+
if not np.isfinite(H) or H <= 0:
|
|
1183
|
+
if verbose:
|
|
1184
|
+
print("Warning: H is not finite or zero in percolation approximation. H =", H)
|
|
1185
|
+
return np.inf
|
|
1186
|
+
if not np.isfinite(vw) or vw <= 0:
|
|
1187
|
+
if verbose:
|
|
1188
|
+
print("Warning: vw is not finite or zero in percolation approximation. vw =", vw)
|
|
1189
|
+
return np.inf
|
|
1190
|
+
crit = np.power(G, 1 / 4.0) / H / betaH / np.power(Iperc / (8 * np.pi * vw**3), 1 / 4.0)
|
|
1191
|
+
return float(np.squeeze(crit)) - 1
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def calcApproxPercolation(
|
|
1195
|
+
outdict: dict,
|
|
1196
|
+
pot,
|
|
1197
|
+
phase_sym,
|
|
1198
|
+
phase_bro,
|
|
1199
|
+
vw: float,
|
|
1200
|
+
verbose: bool = True,
|
|
1201
|
+
tmin: float | None = None,
|
|
1202
|
+
tmax: float | None = None,
|
|
1203
|
+
) -> float:
|
|
1204
|
+
"""Estimate the percolation temperature assuming equal SM/DS temperatures.
|
|
1205
|
+
|
|
1206
|
+
Parameters
|
|
1207
|
+
----------
|
|
1208
|
+
outdict : dict
|
|
1209
|
+
Dictionary storing the action evaluation.
|
|
1210
|
+
pot : generic_potential
|
|
1211
|
+
The effective potential object.
|
|
1212
|
+
phase_sym : PhaseInfo
|
|
1213
|
+
High-temperature phase information.
|
|
1214
|
+
phase_bro : PhaseInfo
|
|
1215
|
+
Low-temperature phase information.
|
|
1216
|
+
vw : float
|
|
1217
|
+
Bubble wall velocity.
|
|
1218
|
+
verbose : bool, optional
|
|
1219
|
+
Whether to emit diagnostic messages.
|
|
1220
|
+
|
|
1221
|
+
Returns
|
|
1222
|
+
-------
|
|
1223
|
+
float
|
|
1224
|
+
The approximate percolation temperature.
|
|
1225
|
+
"""
|
|
1226
|
+
if tmin is None or tmax is None:
|
|
1227
|
+
tmin, tmax = _phase_overlap_interval(pot, phase_sym, phase_bro)
|
|
1228
|
+
|
|
1229
|
+
Tmin = float(tmin)
|
|
1230
|
+
Tmax = float(tmax)
|
|
1231
|
+
if Tmax <= Tmin:
|
|
1232
|
+
raise errors.PercolationApproximation1Error(
|
|
1233
|
+
"No valid temperature bracket for percolation approximation inside "
|
|
1234
|
+
f"the phase-overlap interval [{tmin:.8g}, {tmax:.8g}]."
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
try:
|
|
1238
|
+
# The approximation is only used to place the first adaptive step size grid.
|
|
1239
|
+
# If it fails to bracket a root, fail fast and let the caller choose a
|
|
1240
|
+
# controlled fallback instead of spending many hidden action evaluations.
|
|
1241
|
+
Tperc = optimize.brentq(
|
|
1242
|
+
_approx_percolation_criterion,
|
|
1243
|
+
Tmax,
|
|
1244
|
+
Tmin,
|
|
1245
|
+
rtol=1e-3,
|
|
1246
|
+
args=(outdict, pot, phase_sym, phase_bro, Tmax, Tmin, vw, verbose),
|
|
1247
|
+
)
|
|
1248
|
+
except ValueError as err:
|
|
1249
|
+
if verbose:
|
|
1250
|
+
print("Brentq failed in percolation approximation 1: ", err)
|
|
1251
|
+
raise errors.PercolationApproximation1Error(
|
|
1252
|
+
"Standard percolation approximation failed to bracket a root in "
|
|
1253
|
+
f"[{Tmin:.8g}, {Tmax:.8g}]: {err}"
|
|
1254
|
+
) from err
|
|
1255
|
+
return np.squeeze(Tperc)
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def Tb_criterion(TBRO: float, TSYM: float, phase_sym, phase_broken, pot) -> float:
|
|
1259
|
+
"""Criterion to find the broken phase temperature, it
|
|
1260
|
+
uses energy conservation.
|
|
1261
|
+
Reheat to SM + DS in the broken phase. Assume instant heating.
|
|
1262
|
+
|
|
1263
|
+
Parameters
|
|
1264
|
+
----------
|
|
1265
|
+
TBRO : float
|
|
1266
|
+
Broken phase temperature to solve for
|
|
1267
|
+
TSYM : float
|
|
1268
|
+
Temperature in the symmetric phase
|
|
1269
|
+
phase_sym : PhaseInfo
|
|
1270
|
+
Field value in the broken phase as a function of temperature
|
|
1271
|
+
phase_broken : PhaseInfo
|
|
1272
|
+
Field value in the broken phase as a function of temperature
|
|
1273
|
+
pot : generic_potential
|
|
1274
|
+
The effective potential object
|
|
1275
|
+
|
|
1276
|
+
Returns
|
|
1277
|
+
-------
|
|
1278
|
+
float
|
|
1279
|
+
Criterion: 0 if ``TBRO`` is the correct broken phase temperature.
|
|
1280
|
+
"""
|
|
1281
|
+
|
|
1282
|
+
eSYM = energyDensity(pot, phase_sym, TSYM, include_decoupled=False)
|
|
1283
|
+
eBRO = energyDensity(pot, phase_broken, TBRO, include_decoupled=False)
|
|
1284
|
+
|
|
1285
|
+
return eBRO / eSYM - 1
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def energy_criterion_BRO(TBRO: float, eSYM: float, eBRO_start: float, P: float, dP: float, phase_broken, pot) -> float:
|
|
1289
|
+
"""Calculate the temperature in the broken phase that results
|
|
1290
|
+
from the reheating by converting dP of the false vacuum into true vacuum.
|
|
1291
|
+
|
|
1292
|
+
Parameters
|
|
1293
|
+
----------
|
|
1294
|
+
TBRO : float
|
|
1295
|
+
Temperature to solve for.
|
|
1296
|
+
eSYM : float
|
|
1297
|
+
Energy density in the symmetric phase
|
|
1298
|
+
eBRO_start : float
|
|
1299
|
+
Initial energy density in broken phase
|
|
1300
|
+
P : float
|
|
1301
|
+
True vacuum fraction
|
|
1302
|
+
dP : float
|
|
1303
|
+
Change in the true vacuum fraction
|
|
1304
|
+
phase_broken : PhaseInfo
|
|
1305
|
+
Information about the end phase.
|
|
1306
|
+
pot : generic_potential
|
|
1307
|
+
|
|
1308
|
+
Returns
|
|
1309
|
+
-------
|
|
1310
|
+
float
|
|
1311
|
+
Zero when the criterion is fulfilled.
|
|
1312
|
+
"""
|
|
1313
|
+
eBRO = energyDensity(pot, phase_broken, TBRO, include_decoupled=False)
|
|
1314
|
+
crit = eBRO * P - (eBRO_start * (P - dP) + dP * eSYM)
|
|
1315
|
+
return crit
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def entropy_criterion_SYM_BRO(Tb: float, TBRO_ref: float, TSYM: float, TSYM_ref: float, phase_broken, pot) -> float:
|
|
1319
|
+
"""Calculate the temperature of the dark sector broken phase in terms
|
|
1320
|
+
of the symmetric phase temperature. Only valid when entropy is
|
|
1321
|
+
conserved between reference temperature and TSYM.
|
|
1322
|
+
|
|
1323
|
+
Parameters
|
|
1324
|
+
----------
|
|
1325
|
+
Tb : float
|
|
1326
|
+
Temperature in the broken phase.
|
|
1327
|
+
TBRO_ref : float
|
|
1328
|
+
Reference temperature of DS from which on entropy is conserved.
|
|
1329
|
+
TSYM : float
|
|
1330
|
+
SYM temperature for which we want to know TDS(TSM).
|
|
1331
|
+
TSYM_ref : float
|
|
1332
|
+
Reference temperature of SYM from which on entropy is conserved.
|
|
1333
|
+
phase_broken : PhaseInfo
|
|
1334
|
+
Phase information about the end (broken) phase.
|
|
1335
|
+
pot : generic_potential
|
|
1336
|
+
|
|
1337
|
+
Returns
|
|
1338
|
+
-------
|
|
1339
|
+
float
|
|
1340
|
+
Zero when the condition is met.
|
|
1341
|
+
"""
|
|
1342
|
+
# Working assumption: the broken bubble reheats the DS and the coupled SM
|
|
1343
|
+
# plasma to one local common temperature Tb, while the cosmological scale
|
|
1344
|
+
# factor between successive steps is still inferred from the background
|
|
1345
|
+
# symmetric-phase temperatures TSYM / TSYM_ref. Revisit this hybrid
|
|
1346
|
+
# assumption if the TBRO evolution remains suspect.
|
|
1347
|
+
heff_ds_pt = h_eff_DS(TBRO_ref, pot, phase_broken)
|
|
1348
|
+
heff_SM_pt = td.s_geffSM(TBRO_ref, pot.conversionFactor)
|
|
1349
|
+
heff_ds = h_eff_DS(Tb, pot, phase_broken)
|
|
1350
|
+
heff_SM = td.s_geffSM(Tb, pot.conversionFactor)
|
|
1351
|
+
crit = (heff_ds + heff_SM) * Tb**3
|
|
1352
|
+
crit -= (heff_ds_pt + heff_SM_pt) * TBRO_ref**3 * TSYM**3 / TSYM_ref**3
|
|
1353
|
+
return crit
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def _solve_for_initial_Tperc(
|
|
1359
|
+
state: PercolationState,
|
|
1360
|
+
settings,
|
|
1361
|
+
pot,
|
|
1362
|
+
verbose: bool,
|
|
1363
|
+
) -> float:
|
|
1364
|
+
"""Solve for the percolation temperature after step 2."""
|
|
1365
|
+
Pint = interpolate.interp1d(state.TSYM, state.Pr)
|
|
1366
|
+
try:
|
|
1367
|
+
Tperc_prev = optimize.brentq(lambda T: Pint(T) - settings.f_perc, state.TSYM[0], state.TSYM[-1])
|
|
1368
|
+
except ValueError as err:
|
|
1369
|
+
msg = err.args[0]
|
|
1370
|
+
if msg.startswith("f(a) and f(b) must have different signs"):
|
|
1371
|
+
if Pint(state.TSYM[-1]) < settings.f_perc:
|
|
1372
|
+
explored_tmin = float(state.TSYM[-1]) if state.explored_tmin is None else float(state.explored_tmin)
|
|
1373
|
+
current_tmin = float(state.TSYM[-1])
|
|
1374
|
+
collapse_tol = 1e-12 * max(abs(explored_tmin), abs(current_tmin), 1.0)
|
|
1375
|
+
if explored_tmin + collapse_tol < current_tmin:
|
|
1376
|
+
raise errors.PercolationError(
|
|
1377
|
+
"The percolation-temperature search became numerically unstable: "
|
|
1378
|
+
"the support grid previously explored lower temperatures down to "
|
|
1379
|
+
f"{explored_tmin * pot.conversionFactor:2.8g} GeV, but the final "
|
|
1380
|
+
"step-2 support grid shrank back to Tmin = "
|
|
1381
|
+
f"{current_tmin * pot.conversionFactor:2.8g} GeV before failing "
|
|
1382
|
+
f"with P(Tmin) = {Pint(state.TSYM[-1]):2.5g} < {settings.f_perc:2.5g}. "
|
|
1383
|
+
"This usually indicates a spurious 0->1 jump in P(T); rerun with "
|
|
1384
|
+
"higher percolation resolution."
|
|
1385
|
+
)
|
|
1386
|
+
raise errors.TooMuchSupercoolingError(
|
|
1387
|
+
"The percolation temperature could not be found because the true vacuum "
|
|
1388
|
+
"fraction only reaches "
|
|
1389
|
+
f"{Pint(state.TSYM[-1])} < {settings.f_perc} at Tmin = "
|
|
1390
|
+
f"{state.TSYM[-1] * pot.conversionFactor} GeV."
|
|
1391
|
+
)
|
|
1392
|
+
raise errors.PercolationError(err)
|
|
1393
|
+
except Exception as err:
|
|
1394
|
+
msg = (
|
|
1395
|
+
"Error in calculating Tperc_prev after the second approximation step "
|
|
1396
|
+
"with P = 0 in the Hubble rate. This might be because the nucleation "
|
|
1397
|
+
"criterion could be fulfilled but not the percolation one due to "
|
|
1398
|
+
f"strong supercooling: {err}"
|
|
1399
|
+
)
|
|
1400
|
+
if verbose:
|
|
1401
|
+
print(msg)
|
|
1402
|
+
raise errors.PercolationError(err)
|
|
1403
|
+
if verbose:
|
|
1404
|
+
print(f"Tperc_prev = {Tperc_prev * pot.conversionFactor:2.8g} GeV")
|
|
1405
|
+
return Tperc_prev
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def _action_outdict_temperatures(outdict: dict | None) -> np.ndarray:
|
|
1409
|
+
"""Return the finite action temperatures currently cached in ``outdict``."""
|
|
1410
|
+
if not isinstance(outdict, dict):
|
|
1411
|
+
return np.asarray([], dtype=float)
|
|
1412
|
+
temperatures: list[float] = []
|
|
1413
|
+
for key, payload in outdict.items():
|
|
1414
|
+
if not isinstance(payload, dict) or "action" not in payload:
|
|
1415
|
+
continue
|
|
1416
|
+
try:
|
|
1417
|
+
temperature = float(key)
|
|
1418
|
+
except Exception:
|
|
1419
|
+
continue
|
|
1420
|
+
if np.isfinite(temperature):
|
|
1421
|
+
temperatures.append(float(temperature))
|
|
1422
|
+
return _temperature_grid(temperatures)
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def _transition_action_outdict_temperatures(
|
|
1426
|
+
outdict: dict | None,
|
|
1427
|
+
phase_symmetric,
|
|
1428
|
+
phase_broken,
|
|
1429
|
+
*,
|
|
1430
|
+
tmin: float | None = None,
|
|
1431
|
+
tmax: float | None = None,
|
|
1432
|
+
) -> np.ndarray:
|
|
1433
|
+
"""Return cached action temperatures that clearly belong to one transition."""
|
|
1434
|
+
if not isinstance(outdict, dict):
|
|
1435
|
+
return np.asarray([], dtype=float)
|
|
1436
|
+
|
|
1437
|
+
def phase_point_matches(reference_point, candidate_point) -> bool:
|
|
1438
|
+
try:
|
|
1439
|
+
reference = np.asarray(reference_point, dtype=float)
|
|
1440
|
+
candidate = np.asarray(candidate_point, dtype=float)
|
|
1441
|
+
except Exception:
|
|
1442
|
+
return False
|
|
1443
|
+
if reference.shape != candidate.shape or reference.size == 0:
|
|
1444
|
+
return False
|
|
1445
|
+
if not np.all(np.isfinite(reference)) or not np.all(np.isfinite(candidate)):
|
|
1446
|
+
return False
|
|
1447
|
+
difference = float(np.linalg.norm(reference - candidate))
|
|
1448
|
+
scale = max(float(np.linalg.norm(reference)), float(np.linalg.norm(candidate)), 1.0)
|
|
1449
|
+
return difference <= max(1e-6, 2e-2 * scale)
|
|
1450
|
+
|
|
1451
|
+
lower = -math.inf if tmin is None else float(min(tmin, tmax))
|
|
1452
|
+
upper = math.inf if tmax is None else float(max(tmin, tmax))
|
|
1453
|
+
scale = max(
|
|
1454
|
+
abs(lower) if np.isfinite(lower) else 0.0,
|
|
1455
|
+
abs(upper) if np.isfinite(upper) else 0.0,
|
|
1456
|
+
1.0,
|
|
1457
|
+
)
|
|
1458
|
+
interval_tol = 1e-10 * scale
|
|
1459
|
+
|
|
1460
|
+
temperatures: list[float] = []
|
|
1461
|
+
for key, payload in outdict.items():
|
|
1462
|
+
if not isinstance(payload, dict):
|
|
1463
|
+
continue
|
|
1464
|
+
try:
|
|
1465
|
+
temperature = float(key)
|
|
1466
|
+
except Exception:
|
|
1467
|
+
continue
|
|
1468
|
+
if not np.isfinite(temperature):
|
|
1469
|
+
continue
|
|
1470
|
+
if temperature < lower - interval_tol or temperature > upper + interval_tol:
|
|
1471
|
+
continue
|
|
1472
|
+
|
|
1473
|
+
try:
|
|
1474
|
+
action = float(payload.get("action", np.nan))
|
|
1475
|
+
except Exception:
|
|
1476
|
+
continue
|
|
1477
|
+
if not np.isfinite(action):
|
|
1478
|
+
continue
|
|
1479
|
+
|
|
1480
|
+
high_vev = payload.get("high_vev")
|
|
1481
|
+
low_vev = payload.get("low_vev")
|
|
1482
|
+
if high_vev is not None and low_vev is not None:
|
|
1483
|
+
try:
|
|
1484
|
+
symmetric_point = phase_symmetric.valAt(temperature)
|
|
1485
|
+
broken_point = phase_broken.valAt(temperature)
|
|
1486
|
+
except Exception:
|
|
1487
|
+
pass
|
|
1488
|
+
else:
|
|
1489
|
+
if not (
|
|
1490
|
+
phase_point_matches(high_vev, symmetric_point)
|
|
1491
|
+
and phase_point_matches(low_vev, broken_point)
|
|
1492
|
+
):
|
|
1493
|
+
continue
|
|
1494
|
+
temperatures.append(float(temperature))
|
|
1495
|
+
return _temperature_grid(temperatures)
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
@dataclass
|
|
1499
|
+
class PercolationDiagnostics:
|
|
1500
|
+
"""Percolation diagnostics returned together with the final support bank.
|
|
1501
|
+
|
|
1502
|
+
The solver keeps only diagnostics that are still used by the production
|
|
1503
|
+
adaptive step size percolation workflow and its current validation scripts.
|
|
1504
|
+
"""
|
|
1505
|
+
|
|
1506
|
+
start_temperature: float | None = None
|
|
1507
|
+
final_active_support_points: int = 0
|
|
1508
|
+
total_unique_percolation_support_points: int = 0
|
|
1509
|
+
total_unique_action_temperatures: int = 0
|
|
1510
|
+
rebuild_count: int = 0
|
|
1511
|
+
support_bank_temperatures: list[float] | None = None
|
|
1512
|
+
action_temperatures: list[float] | None = None
|
|
1513
|
+
spline_tnuc: float | None = None
|
|
1514
|
+
spline_tnuc_warning: str | None = None
|
|
1515
|
+
spline_tnuc_max_N: float = math.nan
|
|
1516
|
+
spline_tnuc_integral_prefactor: float = math.nan
|
|
1517
|
+
action_jitter_max_residual_oom: float = math.nan
|
|
1518
|
+
action_jitter_temperature: float | None = None
|
|
1519
|
+
action_jitter_probability: float | None = None
|
|
1520
|
+
action_jitter_log10_gamma_h4: float | None = None
|
|
1521
|
+
action_jitter_log10_gamma_h4_smooth: float | None = None
|
|
1522
|
+
action_jitter_rescue_attempts: int = 0
|
|
1523
|
+
action_jitter_rescue_success: bool = False
|
|
1524
|
+
action_jitter_rescue_temperatures: list[float] | None = None
|
|
1525
|
+
|
|
1526
|
+
@classmethod
|
|
1527
|
+
def from_state(
|
|
1528
|
+
cls,
|
|
1529
|
+
state: PercolationState,
|
|
1530
|
+
grid: PercolationGrid,
|
|
1531
|
+
jitter_diagnostic: dict[str, float | int | bool | list[float] | None] | None,
|
|
1532
|
+
tnuc_estimate: dict[str, float | str | None],
|
|
1533
|
+
) -> "PercolationDiagnostics":
|
|
1534
|
+
"""Build a serialisable diagnostics snapshot from the final solver state."""
|
|
1535
|
+
support_bank = _temperature_grid(state.support_bank)
|
|
1536
|
+
action_temperatures = _temperature_grid(state.action_temperatures)
|
|
1537
|
+
metadata = cls(
|
|
1538
|
+
start_temperature=float(grid.Tstart),
|
|
1539
|
+
final_active_support_points=int(len(np.asarray(state.TSYM, dtype=float))),
|
|
1540
|
+
total_unique_percolation_support_points=int(len(support_bank)),
|
|
1541
|
+
total_unique_action_temperatures=int(len(action_temperatures)),
|
|
1542
|
+
rebuild_count=int(state.rebuild_count),
|
|
1543
|
+
support_bank_temperatures=[float(value) for value in support_bank.tolist()],
|
|
1544
|
+
action_temperatures=[float(value) for value in action_temperatures.tolist()],
|
|
1545
|
+
)
|
|
1546
|
+
if jitter_diagnostic:
|
|
1547
|
+
max_residual = jitter_diagnostic.get("max_residual_oom", math.nan)
|
|
1548
|
+
metadata.action_jitter_max_residual_oom = (
|
|
1549
|
+
float(max_residual) if max_residual is not None else math.nan
|
|
1550
|
+
)
|
|
1551
|
+
metadata.action_jitter_temperature = (
|
|
1552
|
+
float(jitter_diagnostic["temperature"])
|
|
1553
|
+
if jitter_diagnostic.get("temperature") is not None
|
|
1554
|
+
else None
|
|
1555
|
+
)
|
|
1556
|
+
metadata.action_jitter_probability = (
|
|
1557
|
+
float(jitter_diagnostic["probability"])
|
|
1558
|
+
if jitter_diagnostic.get("probability") is not None
|
|
1559
|
+
else None
|
|
1560
|
+
)
|
|
1561
|
+
metadata.action_jitter_log10_gamma_h4 = (
|
|
1562
|
+
float(jitter_diagnostic["log10_gamma_h4"])
|
|
1563
|
+
if jitter_diagnostic.get("log10_gamma_h4") is not None
|
|
1564
|
+
else None
|
|
1565
|
+
)
|
|
1566
|
+
metadata.action_jitter_log10_gamma_h4_smooth = (
|
|
1567
|
+
float(jitter_diagnostic["log10_gamma_h4_smooth"])
|
|
1568
|
+
if jitter_diagnostic.get("log10_gamma_h4_smooth") is not None
|
|
1569
|
+
else None
|
|
1570
|
+
)
|
|
1571
|
+
metadata.action_jitter_rescue_attempts = int(jitter_diagnostic.get("rescue_attempts", 0) or 0)
|
|
1572
|
+
metadata.action_jitter_rescue_success = bool(jitter_diagnostic.get("rescue_success", False))
|
|
1573
|
+
metadata.action_jitter_rescue_temperatures = [
|
|
1574
|
+
float(value) for value in (jitter_diagnostic.get("rescue_temperatures") or [])
|
|
1575
|
+
]
|
|
1576
|
+
|
|
1577
|
+
if tnuc_estimate.get("Tnuc") is not None:
|
|
1578
|
+
metadata.spline_tnuc = float(tnuc_estimate["Tnuc"])
|
|
1579
|
+
metadata.spline_tnuc_warning = (
|
|
1580
|
+
str(tnuc_estimate["warning"]) if tnuc_estimate.get("warning") is not None else None
|
|
1581
|
+
)
|
|
1582
|
+
max_N = tnuc_estimate.get("max_N", math.nan)
|
|
1583
|
+
prefactor = tnuc_estimate.get("integral_prefactor", math.nan)
|
|
1584
|
+
metadata.spline_tnuc_max_N = float(max_N) if max_N is not None else math.nan
|
|
1585
|
+
metadata.spline_tnuc_integral_prefactor = float(prefactor) if prefactor is not None else math.nan
|
|
1586
|
+
return metadata
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
def _action_rate_jitter_diagnostic(
|
|
1590
|
+
T: np.ndarray,
|
|
1591
|
+
H: np.ndarray,
|
|
1592
|
+
S: np.ndarray,
|
|
1593
|
+
P: np.ndarray,
|
|
1594
|
+
) -> dict[str, float | int | None]:
|
|
1595
|
+
"""Measure non-smooth jumps in the active-band rate samples."""
|
|
1596
|
+
from transitionlistener.percolation_adaptive_rate import _log10_gamma_h4_array
|
|
1597
|
+
|
|
1598
|
+
p_low = 0.01
|
|
1599
|
+
p_high = 0.9
|
|
1600
|
+
T = np.asarray(T, dtype=float)
|
|
1601
|
+
H = np.asarray(H, dtype=float)
|
|
1602
|
+
S = np.asarray(S, dtype=float)
|
|
1603
|
+
P = np.asarray(P, dtype=float)
|
|
1604
|
+
log10_rate = _log10_gamma_h4_array(T, S, H)
|
|
1605
|
+
mask = (
|
|
1606
|
+
np.isfinite(T)
|
|
1607
|
+
& np.isfinite(H)
|
|
1608
|
+
& np.isfinite(S)
|
|
1609
|
+
& np.isfinite(P)
|
|
1610
|
+
& np.isfinite(log10_rate)
|
|
1611
|
+
& (T > 0.0)
|
|
1612
|
+
& (H > 0.0)
|
|
1613
|
+
& (P >= p_low)
|
|
1614
|
+
& (P <= p_high)
|
|
1615
|
+
)
|
|
1616
|
+
active_indices = np.flatnonzero(mask)
|
|
1617
|
+
if active_indices.size < 5:
|
|
1618
|
+
return {"n_active": int(active_indices.size), "max_residual_oom": math.nan}
|
|
1619
|
+
|
|
1620
|
+
x = np.log(T[active_indices])
|
|
1621
|
+
y = log10_rate[active_indices]
|
|
1622
|
+
order = np.argsort(x)
|
|
1623
|
+
x = x[order]
|
|
1624
|
+
y = y[order]
|
|
1625
|
+
indices = active_indices[order]
|
|
1626
|
+
if np.ptp(x) <= 1.0e-12:
|
|
1627
|
+
return {"n_active": int(active_indices.size), "max_residual_oom": math.nan}
|
|
1628
|
+
|
|
1629
|
+
degree = 2 if x.size >= 3 else 1
|
|
1630
|
+
try:
|
|
1631
|
+
coeff = np.polyfit(x, y, degree)
|
|
1632
|
+
smooth = np.polyval(coeff, x)
|
|
1633
|
+
except Exception:
|
|
1634
|
+
return {"n_active": int(active_indices.size), "max_residual_oom": math.nan}
|
|
1635
|
+
residual = y - smooth
|
|
1636
|
+
if residual.size == 0 or not np.any(np.isfinite(residual)):
|
|
1637
|
+
return {"n_active": int(active_indices.size), "max_residual_oom": math.nan}
|
|
1638
|
+
local_idx = int(np.nanargmax(np.abs(residual)))
|
|
1639
|
+
source_idx = int(indices[local_idx])
|
|
1640
|
+
return {
|
|
1641
|
+
"n_active": int(active_indices.size),
|
|
1642
|
+
"max_residual_oom": float(abs(residual[local_idx])),
|
|
1643
|
+
"signed_residual_oom": float(residual[local_idx]),
|
|
1644
|
+
"temperature": float(T[source_idx]),
|
|
1645
|
+
"probability": float(P[source_idx]),
|
|
1646
|
+
"log10_gamma_h4": float(log10_rate[source_idx]),
|
|
1647
|
+
"log10_gamma_h4_smooth": float(smooth[local_idx]),
|
|
1648
|
+
"p_low": float(p_low),
|
|
1649
|
+
"p_high": float(p_high),
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
def _raise_if_action_rate_jitter_unresolved(
|
|
1654
|
+
state: PercolationState,
|
|
1655
|
+
settings,
|
|
1656
|
+
CF: float,
|
|
1657
|
+
pot=None,
|
|
1658
|
+
phase_symmetric=None,
|
|
1659
|
+
phase_broken=None,
|
|
1660
|
+
outdict: dict | None = None,
|
|
1661
|
+
) -> dict[str, float | int | None]:
|
|
1662
|
+
"""Reject accepted profiles whose active-band rate data is visibly unstable."""
|
|
1663
|
+
diagnostic = _action_rate_jitter_diagnostic(state.TSYM, state.Hr, state.Sr, state.Pr)
|
|
1664
|
+
threshold = float(settings.jitter_GH4_threshold)
|
|
1665
|
+
residual = diagnostic.get("max_residual_oom")
|
|
1666
|
+
if (
|
|
1667
|
+
residual is not None
|
|
1668
|
+
and np.isfinite(float(residual))
|
|
1669
|
+
and float(residual) > threshold
|
|
1670
|
+
and bool(getattr(settings, "jitter_rescue", False))
|
|
1671
|
+
and int(getattr(settings, "n_jitter_save", 0)) > 0
|
|
1672
|
+
and pot is not None
|
|
1673
|
+
and phase_symmetric is not None
|
|
1674
|
+
and phase_broken is not None
|
|
1675
|
+
and isinstance(outdict, dict)
|
|
1676
|
+
):
|
|
1677
|
+
diagnostic = _try_action_jitter_tunneltight_rescue(
|
|
1678
|
+
state,
|
|
1679
|
+
settings,
|
|
1680
|
+
diagnostic,
|
|
1681
|
+
threshold,
|
|
1682
|
+
pot,
|
|
1683
|
+
phase_symmetric,
|
|
1684
|
+
phase_broken,
|
|
1685
|
+
outdict,
|
|
1686
|
+
)
|
|
1687
|
+
residual = diagnostic.get("max_residual_oom")
|
|
1688
|
+
if residual is None or not np.isfinite(float(residual)) or float(residual) <= threshold:
|
|
1689
|
+
return diagnostic
|
|
1690
|
+
|
|
1691
|
+
temperature_value = diagnostic.get("temperature")
|
|
1692
|
+
probability_value = diagnostic.get("probability")
|
|
1693
|
+
log10_rate_value = diagnostic.get("log10_gamma_h4")
|
|
1694
|
+
smooth_value = diagnostic.get("log10_gamma_h4_smooth")
|
|
1695
|
+
temperature = float(temperature_value) if temperature_value is not None else math.nan
|
|
1696
|
+
probability = float(probability_value) if probability_value is not None else math.nan
|
|
1697
|
+
log10_rate = float(log10_rate_value) if log10_rate_value is not None else math.nan
|
|
1698
|
+
smooth = float(smooth_value) if smooth_value is not None else math.nan
|
|
1699
|
+
message = (
|
|
1700
|
+
"Detected non-smooth bounce-rate data in the active percolation band "
|
|
1701
|
+
f"{diagnostic.get('p_low', 0.01):.3g} <= P(T) <= "
|
|
1702
|
+
f"{diagnostic.get('p_high', 0.9):.3g}: "
|
|
1703
|
+
f"the largest quadratic-fit residual in log10(Gamma/H^4) is "
|
|
1704
|
+
f"{float(residual):.3g} orders of magnitude, above the configured "
|
|
1705
|
+
f"threshold {threshold:.3g}. The offending support point is at "
|
|
1706
|
+
f"T = {temperature * CF:.6g} GeV (internal T = {temperature:.8g}), "
|
|
1707
|
+
f"P = {probability:.6g}, log10(Gamma/H^4) = {log10_rate:.6g}, while the "
|
|
1708
|
+
f"local smooth fit predicts {smooth:.6g}. This indicates an instability "
|
|
1709
|
+
"of the bounce computation or a wrong tunnelling branch, not a trustworthy "
|
|
1710
|
+
"percolation feature. Suggested fixes: rerun with more robust/benchmark "
|
|
1711
|
+
"tunnelling accuracy, improve phase tracing near this temperature, inspect "
|
|
1712
|
+
"the action bank, or increase the bounce solver precision."
|
|
1713
|
+
)
|
|
1714
|
+
err = errors.ActionRateJitterError(message)
|
|
1715
|
+
setattr(err, "action_rate_jitter_diagnostic", diagnostic)
|
|
1716
|
+
raise err
|
|
1717
|
+
|
|
1718
|
+
|
|
1719
|
+
def _matching_outdict_key(outdict: dict, temperature: float) -> object | None:
|
|
1720
|
+
"""Return an outdict key matching ``temperature`` up to floating roundoff."""
|
|
1721
|
+
if temperature in outdict:
|
|
1722
|
+
return temperature
|
|
1723
|
+
for key, payload in outdict.items():
|
|
1724
|
+
if not isinstance(payload, dict) or "action" not in payload:
|
|
1725
|
+
continue
|
|
1726
|
+
try:
|
|
1727
|
+
key_float = float(key)
|
|
1728
|
+
except Exception:
|
|
1729
|
+
continue
|
|
1730
|
+
if np.isclose(key_float, temperature, rtol=1.0e-11, atol=1.0e-12):
|
|
1731
|
+
return key
|
|
1732
|
+
return None
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def _try_action_jitter_tunneltight_rescue(
|
|
1736
|
+
state: PercolationState,
|
|
1737
|
+
settings,
|
|
1738
|
+
diagnostic: dict[str, float | int | None],
|
|
1739
|
+
threshold: float,
|
|
1740
|
+
pot,
|
|
1741
|
+
phase_symmetric,
|
|
1742
|
+
phase_broken,
|
|
1743
|
+
outdict: dict,
|
|
1744
|
+
) -> dict[str, float | int | None]:
|
|
1745
|
+
"""Opt-in recomputation of jitter outlier actions with tunneltight settings."""
|
|
1746
|
+
max_attempts = max(int(getattr(settings, "n_jitter_save", 0)), 0)
|
|
1747
|
+
if max_attempts <= 0:
|
|
1748
|
+
return diagnostic
|
|
1749
|
+
attempted_temperatures: list[float] = []
|
|
1750
|
+
attempted_keys: set[float] = set()
|
|
1751
|
+
original_tunneling = copy.deepcopy(pot.config.tracingConf.tunneling_params)
|
|
1752
|
+
tight_tunneling = copy.deepcopy(original_tunneling)
|
|
1753
|
+
deform = dict(tight_tunneling.get("deformation_deform_params", {}))
|
|
1754
|
+
deform["converge_0"] = 1.0
|
|
1755
|
+
deform["fRatioConv"] = 5.0e-3
|
|
1756
|
+
tight_tunneling["deformation_deform_params"] = deform
|
|
1757
|
+
|
|
1758
|
+
for _attempt in range(max_attempts):
|
|
1759
|
+
residual = diagnostic.get("max_residual_oom")
|
|
1760
|
+
temperature_value = diagnostic.get("temperature")
|
|
1761
|
+
if (
|
|
1762
|
+
residual is None
|
|
1763
|
+
or not np.isfinite(float(residual))
|
|
1764
|
+
or float(residual) <= threshold
|
|
1765
|
+
or temperature_value is None
|
|
1766
|
+
):
|
|
1767
|
+
break
|
|
1768
|
+
temperature = float(temperature_value)
|
|
1769
|
+
rounded_key = round(temperature, 12)
|
|
1770
|
+
if rounded_key in attempted_keys:
|
|
1771
|
+
break
|
|
1772
|
+
attempted_keys.add(rounded_key)
|
|
1773
|
+
attempted_temperatures.append(temperature)
|
|
1774
|
+
idx = int(np.nanargmin(np.abs(np.asarray(state.TSYM, dtype=float) - temperature)))
|
|
1775
|
+
old_key = _matching_outdict_key(outdict, temperature)
|
|
1776
|
+
old_payload = outdict.pop(old_key, None) if old_key is not None else None
|
|
1777
|
+
if old_payload is not None:
|
|
1778
|
+
unstable = outdict.setdefault("_unstable_action_entries", [])
|
|
1779
|
+
if isinstance(unstable, list):
|
|
1780
|
+
unstable.append(
|
|
1781
|
+
{
|
|
1782
|
+
"T": float(temperature),
|
|
1783
|
+
"action": old_payload.get("action") if isinstance(old_payload, dict) else None,
|
|
1784
|
+
"reason": "action_rate_jitter_tunneltight_rescue",
|
|
1785
|
+
"max_residual_oom": float(residual),
|
|
1786
|
+
}
|
|
1787
|
+
)
|
|
1788
|
+
try:
|
|
1789
|
+
pot.config.tracingConf.tunneling_params = tight_tunneling
|
|
1790
|
+
state.Sr[idx] = calcAction(pot, temperature, phase_symmetric, phase_broken, outdict)
|
|
1791
|
+
except Exception:
|
|
1792
|
+
if old_key is not None and old_payload is not None:
|
|
1793
|
+
outdict[old_key] = old_payload
|
|
1794
|
+
break
|
|
1795
|
+
finally:
|
|
1796
|
+
pot.config.tracingConf.tunneling_params = original_tunneling
|
|
1797
|
+
diagnostic = _action_rate_jitter_diagnostic(state.TSYM, state.Hr, state.Sr, state.Pr)
|
|
1798
|
+
|
|
1799
|
+
diagnostic = dict(diagnostic)
|
|
1800
|
+
diagnostic["rescue_attempts"] = int(len(attempted_temperatures))
|
|
1801
|
+
diagnostic["rescue_temperatures"] = [float(value) for value in attempted_temperatures]
|
|
1802
|
+
residual = diagnostic.get("max_residual_oom")
|
|
1803
|
+
diagnostic["rescue_success"] = bool(
|
|
1804
|
+
residual is not None and np.isfinite(float(residual)) and float(residual) <= threshold
|
|
1805
|
+
)
|
|
1806
|
+
return diagnostic
|
|
1807
|
+
|
|
1808
|
+
|
|
1809
|
+
def estimate_spline_tnuc_from_rate_history(
|
|
1810
|
+
T: np.ndarray,
|
|
1811
|
+
H: np.ndarray,
|
|
1812
|
+
S: np.ndarray,
|
|
1813
|
+
P: np.ndarray,
|
|
1814
|
+
vw: float = 1.0,
|
|
1815
|
+
) -> dict[str, float | str | None]:
|
|
1816
|
+
r"""Estimate ``Tnuc`` from the saved rate/P/H splines without new actions.
|
|
1817
|
+
|
|
1818
|
+
The diagnostic solves
|
|
1819
|
+
|
|
1820
|
+
``N(T) = 4*pi/3 * v_w^3 * int_T^Tinit Gamma/H^4 * (1-P) dT/T = 1``
|
|
1821
|
+
|
|
1822
|
+
on a dense spline grid. It is intentionally warning-only: if the final
|
|
1823
|
+
action bank does not bracket ``N=1`` the caller can still return a valid
|
|
1824
|
+
percolation result and expose the failure category in metadata.
|
|
1825
|
+
"""
|
|
1826
|
+
try:
|
|
1827
|
+
T = np.asarray(T, dtype=float).reshape(-1)
|
|
1828
|
+
H = np.asarray(H, dtype=float).reshape(-1)
|
|
1829
|
+
S = np.asarray(S, dtype=float).reshape(-1)
|
|
1830
|
+
P = np.asarray(P, dtype=float).reshape(-1)
|
|
1831
|
+
log_rate = np.asarray(logGamma(T, S), dtype=float) - 4.0 * np.log(H)
|
|
1832
|
+
mask = (
|
|
1833
|
+
np.isfinite(T)
|
|
1834
|
+
& np.isfinite(H)
|
|
1835
|
+
& np.isfinite(S)
|
|
1836
|
+
& np.isfinite(P)
|
|
1837
|
+
& np.isfinite(log_rate)
|
|
1838
|
+
& (T > 0.0)
|
|
1839
|
+
& (H > 0.0)
|
|
1840
|
+
)
|
|
1841
|
+
if np.count_nonzero(mask) < 3:
|
|
1842
|
+
return {"Tnuc": None, "warning": "spline_tnuc_unavailable", "max_N": math.nan}
|
|
1843
|
+
|
|
1844
|
+
u = np.log(T[mask])
|
|
1845
|
+
rate = log_rate[mask]
|
|
1846
|
+
prob = np.clip(P[mask], 0.0, 1.0)
|
|
1847
|
+
order = np.argsort(u)
|
|
1848
|
+
u = u[order]
|
|
1849
|
+
rate = rate[order]
|
|
1850
|
+
prob = prob[order]
|
|
1851
|
+
unique = np.concatenate(([True], np.diff(u) > 1.0e-12))
|
|
1852
|
+
u = u[unique]
|
|
1853
|
+
rate = rate[unique]
|
|
1854
|
+
prob = prob[unique]
|
|
1855
|
+
if u.size < 3 or np.ptp(u) <= 1.0e-12:
|
|
1856
|
+
return {"Tnuc": None, "warning": "spline_tnuc_unavailable", "max_N": math.nan}
|
|
1857
|
+
|
|
1858
|
+
rate_spline = interpolate.PchipInterpolator(u, rate, extrapolate=False)
|
|
1859
|
+
prob_spline = interpolate.PchipInterpolator(u, prob, extrapolate=False)
|
|
1860
|
+
n_grid = max(1000, 20 * int(u.size))
|
|
1861
|
+
u_dense = np.linspace(float(u[0]), float(u[-1]), n_grid)
|
|
1862
|
+
rate_dense = np.asarray(rate_spline(u_dense), dtype=float)
|
|
1863
|
+
prob_dense = np.clip(np.asarray(prob_spline(u_dense), dtype=float), 0.0, 1.0)
|
|
1864
|
+
with np.errstate(over="ignore", invalid="ignore"):
|
|
1865
|
+
integrand = np.exp(np.clip(rate_dense, -745.0, 700.0)) * (1.0 - prob_dense)
|
|
1866
|
+
integrand[~np.isfinite(integrand)] = 0.0
|
|
1867
|
+
cumulative_from_cold = integrate.cumulative_trapezoid(integrand, x=u_dense, initial=0.0)
|
|
1868
|
+
total = float(cumulative_from_cold[-1])
|
|
1869
|
+
prefactor = 4.0 * np.pi / 3.0 * float(vw) ** 3
|
|
1870
|
+
N_dense = prefactor * (total - cumulative_from_cold)
|
|
1871
|
+
max_N = float(np.nanmax(N_dense)) if N_dense.size else math.nan
|
|
1872
|
+
if not np.isfinite(max_N):
|
|
1873
|
+
return {"Tnuc": None, "warning": "spline_tnuc_failed", "max_N": math.nan}
|
|
1874
|
+
if max_N < 1.0:
|
|
1875
|
+
return {
|
|
1876
|
+
"Tnuc": None,
|
|
1877
|
+
"warning": "spline_tnuc_not_reached",
|
|
1878
|
+
"max_N": max_N,
|
|
1879
|
+
"integral_prefactor": prefactor,
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
u_hot_to_cold = u_dense[::-1]
|
|
1883
|
+
N_hot_to_cold = N_dense[::-1]
|
|
1884
|
+
crossing = np.flatnonzero(N_hot_to_cold >= 1.0)
|
|
1885
|
+
if crossing.size == 0:
|
|
1886
|
+
return {"Tnuc": None, "warning": "spline_tnuc_failed", "max_N": max_N}
|
|
1887
|
+
idx = int(crossing[0])
|
|
1888
|
+
if idx == 0:
|
|
1889
|
+
u_cross = float(u_hot_to_cold[0])
|
|
1890
|
+
else:
|
|
1891
|
+
n0 = float(N_hot_to_cold[idx - 1])
|
|
1892
|
+
n1 = float(N_hot_to_cold[idx])
|
|
1893
|
+
u0 = float(u_hot_to_cold[idx - 1])
|
|
1894
|
+
u1 = float(u_hot_to_cold[idx])
|
|
1895
|
+
if abs(n1 - n0) <= 1.0e-300:
|
|
1896
|
+
u_cross = u1
|
|
1897
|
+
else:
|
|
1898
|
+
frac = (1.0 - n0) / (n1 - n0)
|
|
1899
|
+
u_cross = u0 + frac * (u1 - u0)
|
|
1900
|
+
return {
|
|
1901
|
+
"Tnuc": float(np.exp(u_cross)),
|
|
1902
|
+
"warning": None,
|
|
1903
|
+
"max_N": max_N,
|
|
1904
|
+
"integral_prefactor": prefactor,
|
|
1905
|
+
}
|
|
1906
|
+
except Exception:
|
|
1907
|
+
return {"Tnuc": None, "warning": "spline_tnuc_failed", "max_N": math.nan}
|
|
1908
|
+
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
def calc_betaH_S3(T: float, Sint: interpolate.interp1d, outdict: dict, pot, phase_sym, phase_bro, verbose=False) -> float:
|
|
1912
|
+
"""Calculate the phase transition speed from the action derivative.
|
|
1913
|
+
|
|
1914
|
+
Parameters
|
|
1915
|
+
----------
|
|
1916
|
+
T : float
|
|
1917
|
+
The temperature at which to evaluate beta/H
|
|
1918
|
+
Sint: interpolate.interp1d
|
|
1919
|
+
Interpolation function of the action
|
|
1920
|
+
outdict : dict
|
|
1921
|
+
Dictionary storing the action evaluations, key is `T`
|
|
1922
|
+
pot : generic_potential
|
|
1923
|
+
The effective potential object
|
|
1924
|
+
phase_sym : PhaseInfo
|
|
1925
|
+
Information about the high temperature (symmetric) phase
|
|
1926
|
+
phase_bro : PhaseInfo
|
|
1927
|
+
Information about the low temperature (broken) phaes
|
|
1928
|
+
|
|
1929
|
+
Returns
|
|
1930
|
+
----------
|
|
1931
|
+
float :
|
|
1932
|
+
The transition speed beta/H."""
|
|
1933
|
+
dT = T * 1e-5
|
|
1934
|
+
tmin = max(float(phase_sym.Tmin), float(phase_bro.Tmin))
|
|
1935
|
+
tmax = min(float(phase_sym.Tmax), float(phase_bro.Tmax))
|
|
1936
|
+
if tmin >= tmax or T <= tmin or T >= tmax:
|
|
1937
|
+
if verbose:
|
|
1938
|
+
print(
|
|
1939
|
+
"Warning: cannot evaluate betaH outside phase overlap. "
|
|
1940
|
+
f"T={T:.8g}, overlap=[{tmin:.8g}, {tmax:.8g}]"
|
|
1941
|
+
)
|
|
1942
|
+
return np.nan
|
|
1943
|
+
# Check if the derivative can actually be calculated
|
|
1944
|
+
# in the range of interest. If not, make the points
|
|
1945
|
+
# support for the derivative closer to each other.
|
|
1946
|
+
if T - 2 * dT < tmin or T + 2 * dT > tmax:
|
|
1947
|
+
dT = np.minimum((T - tmin) / 100, (tmax - T) / 100)
|
|
1948
|
+
|
|
1949
|
+
if len(Sint.x) >= 10 and Sint.x[0] < T - 2 * dT and Sint.x[-1] > T + 2 * dT:
|
|
1950
|
+
dSdT = derivative(Sint, T, dT)
|
|
1951
|
+
betaH = dSdT - Sint(T) / T
|
|
1952
|
+
else:
|
|
1953
|
+
# not enough evaluations of S for accuracy
|
|
1954
|
+
Tr = []
|
|
1955
|
+
Sr = []
|
|
1956
|
+
for i in range(-2, 3, 1):
|
|
1957
|
+
S = calcAction(pot, T - dT * i, phase_sym, phase_bro, outdict)
|
|
1958
|
+
Tr.append(T - dT * i)
|
|
1959
|
+
Sr.append(S)
|
|
1960
|
+
T_ar = np.array(Tr[::-1]) # We need increasing T for the spline interpolation
|
|
1961
|
+
S_ar = np.array(Sr[::-1])
|
|
1962
|
+
if np.all(np.isinf(S_ar)):
|
|
1963
|
+
if verbose:
|
|
1964
|
+
print("WARNING: All elements of S_ar are infinite. Unable to proceed with calculation on beta/H.")
|
|
1965
|
+
print(
|
|
1966
|
+
"Most likely the transition is so weak that the action is infinite. "
|
|
1967
|
+
"Correspondingly, beta/H is also set to inf."
|
|
1968
|
+
)
|
|
1969
|
+
return np.inf
|
|
1970
|
+
tckS = interpolate.splrep(T_ar, S_ar, s=0)
|
|
1971
|
+
S = interpolate.splev(T, tckS, der=0)
|
|
1972
|
+
dSdT = interpolate.splev(T, tckS, der=1)
|
|
1973
|
+
betaH = dSdT - S / T
|
|
1974
|
+
return betaH
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
def calcAlphas(T: float, pot, high_phase, low_phase, verbose=False) -> tuple[float]:
|
|
1978
|
+
"""Calculate the total transition strenght of the PT.
|
|
1979
|
+
Use several definitions.
|
|
1980
|
+
|
|
1981
|
+
The last 3 alphas are normalised to the radiation energy density of only
|
|
1982
|
+
the relevant sector for the bubble expansion. The hydro coupling is read
|
|
1983
|
+
from ``pot.config.gwConf.coupled_hydrodynamics``.
|
|
1984
|
+
|
|
1985
|
+
Parameters
|
|
1986
|
+
----------
|
|
1987
|
+
T : float
|
|
1988
|
+
Temperature at which to evaluate alpha
|
|
1989
|
+
|
|
1990
|
+
Returns
|
|
1991
|
+
----------
|
|
1992
|
+
tuple : float
|
|
1993
|
+
``(alpha_p, alpha_theta, alpha_thetabar, alpha_e, alpha_hyd, alpha_inf,
|
|
1994
|
+
alpha_eq)``. ``alpha_theta`` is the bag-model strength fed to the
|
|
1995
|
+
GW-signal / kappa() pipeline; ``alpha_thetabar`` is the beyond-bag
|
|
1996
|
+
pseudo-trace-anomaly definition from arXiv:2004.06995."""
|
|
1997
|
+
|
|
1998
|
+
high_phi = high_phase.valAt(T) # Start phase phi values
|
|
1999
|
+
low_phi = low_phase.valAt(T) # End phase phi values
|
|
2000
|
+
DeltaV = np.abs(pot.Vtot(high_phi, T) - pot.Vtot(low_phi, T))
|
|
2001
|
+
|
|
2002
|
+
# Derivative of the potential with respect to T
|
|
2003
|
+
dT = T * 1e-5
|
|
2004
|
+
dDeltaV_p = np.abs(pot.Vtot(high_phi, T + dT / 2) - pot.Vtot(low_phi, T + dT / 2))
|
|
2005
|
+
dDeltaV_m = np.abs(pot.Vtot(high_phi, T - dT / 2) - pot.Vtot(low_phi, T - dT / 2))
|
|
2006
|
+
dDeltaVdT = (dDeltaV_p - dDeltaV_m) / dT
|
|
2007
|
+
|
|
2008
|
+
# Use the symmetric-phase radiation bath when normalizing the release.
|
|
2009
|
+
# This differs slightly from the older broken-phase normalization below
|
|
2010
|
+
# roughly 100 MeV.
|
|
2011
|
+
rho_rad_PTsector = pot.radiationEnergyDensity(high_phi, T, include_decoupled=False)
|
|
2012
|
+
# With include_decoupled=False, sectors thermally decoupled from the PT
|
|
2013
|
+
# sector are excluded from this normalization.
|
|
2014
|
+
|
|
2015
|
+
# Here we assume that the decoupled sector has the same temperature as the PT sector
|
|
2016
|
+
rho_rad_tot = pot.radiationEnergyDensity(high_phi, T, include_decoupled=True)
|
|
2017
|
+
|
|
2018
|
+
# Energy density:
|
|
2019
|
+
DeltaE = DeltaV - T * dDeltaVdT
|
|
2020
|
+
|
|
2021
|
+
# Alpha definitions:
|
|
2022
|
+
alpha_p = DeltaV / rho_rad_tot
|
|
2023
|
+
alpha_theta = (DeltaE + 3 * DeltaV) / (4 * rho_rad_tot)
|
|
2024
|
+
alpha_e = DeltaE / rho_rad_tot
|
|
2025
|
+
|
|
2026
|
+
# Beyond-bag pseudo-trace-anomaly alpha from arXiv:2004.06995 (Giese-KKS).
|
|
2027
|
+
# Use the same effective-potential normalization as energyDensity(); otherwise
|
|
2028
|
+
# theta_bar and its enthalpy normalization depend on the arbitrary Vtot offset.
|
|
2029
|
+
# V0_ref = pot.V0(pot.X0) + pot.Vct(pot.X0) + pot.V1_from_X(pot.X0)
|
|
2030
|
+
# Veff_sym = pot.Vtot(high_phi, T) - V0_ref
|
|
2031
|
+
# Veff_bro = pot.Vtot(low_phi, T) - V0_ref
|
|
2032
|
+
# csSq_sym = calcSoundSpeedSq(pot, high_phi, T)
|
|
2033
|
+
# theta_sym = pot.energyDensity(high_phi, T) + Veff_sym / csSq_sym
|
|
2034
|
+
# csSq_bro = calcSoundSpeedSq(pot, low_phi, T)
|
|
2035
|
+
# theta_bro = pot.energyDensity(low_phi, T) + Veff_bro / csSq_bro
|
|
2036
|
+
# alpha_thetabar_old = (theta_sym - theta_bro) / (3 * (-Veff_sym + pot.energyDensity(high_phi, T)))
|
|
2037
|
+
|
|
2038
|
+
V0_ref = pot.V0(pot.X0) + pot.Vct(pot.X0) + pot.V1_from_X(pot.X0)
|
|
2039
|
+
Veff_sym = pot.Vtot(high_phi, T) - V0_ref
|
|
2040
|
+
Veff_bro = pot.Vtot(low_phi, T) - V0_ref
|
|
2041
|
+
csSq_sym = calcSoundSpeedSq(pot, high_phi, T)
|
|
2042
|
+
theta_sym = -T*pot.dVdT(high_phi, T, dT=dT) + Veff_sym * (1 + 1/ csSq_sym)
|
|
2043
|
+
csSq_bro = calcSoundSpeedSq(pot, low_phi, T)
|
|
2044
|
+
theta_bro = -T*pot.dVdT(low_phi, T, dT=dT) + Veff_bro * (1 + 1/ csSq_bro)
|
|
2045
|
+
dedT = (pot.energyDensity(high_phi, T + dT) - pot.energyDensity(high_phi, T - dT))/(2*dT)
|
|
2046
|
+
alpha_thetabar = (theta_sym - theta_bro) / (3* csSq_sym * T * dedT)
|
|
2047
|
+
|
|
2048
|
+
bosons_low = pot.boson_massSq(low_phi, 0) # low-T phase masses
|
|
2049
|
+
bosons_high = pot.boson_massSq(high_phi, 0) # high-T phase masses
|
|
2050
|
+
fermions_low = pot.fermion_massSq(low_phi)
|
|
2051
|
+
fermions_high = pot.fermion_massSq(high_phi)
|
|
2052
|
+
|
|
2053
|
+
# alpha_inf
|
|
2054
|
+
gauge_coupling = pot.mass_spectrum.boson_gauge_couplings
|
|
2055
|
+
m2_bos_after, dof_bos, _, is_physical = bosons_low
|
|
2056
|
+
m2_bos_before, _, _, _ = bosons_high
|
|
2057
|
+
m2_fer_after, dof_fer = fermions_low
|
|
2058
|
+
m2_fer_before, _ = fermions_high
|
|
2059
|
+
|
|
2060
|
+
delta_m2_bos = np.maximum(m2_bos_after - m2_bos_before, 0)
|
|
2061
|
+
m2factor = np.sum(dof_bos * is_physical * delta_m2_bos, axis=-1) / 24.0
|
|
2062
|
+
|
|
2063
|
+
delta_m2_fer = np.maximum(m2_fer_after - m2_fer_before, 0)
|
|
2064
|
+
m2factor += np.sum(dof_fer * delta_m2_fer, axis=-1) / 48.0
|
|
2065
|
+
|
|
2066
|
+
m_bos_after = np.sqrt(np.where(m2_bos_after > 0, m2_bos_after, 0))
|
|
2067
|
+
# Avoid sqrt of negative mass squares by setting negatives to zero.
|
|
2068
|
+
# This only occurs for Goldstones, which are excluded via is_physical = 0.
|
|
2069
|
+
m_bos_before = np.sqrt(np.where(m2_bos_before > 0, m2_bos_before, 0))
|
|
2070
|
+
delta_m_bos = np.maximum(m_bos_after - m_bos_before, 0)
|
|
2071
|
+
|
|
2072
|
+
# alpha_eq, see eq. (2.16) in 1903.09642
|
|
2073
|
+
# the hydrodynamic alphas do not depend on the decoupled radiation bath!
|
|
2074
|
+
alpha_eq = T**3 / rho_rad_PTsector * np.sum(delta_m_bos * gauge_coupling**2 * dof_bos * is_physical, axis=-1)
|
|
2075
|
+
alpha_inf = T**2 / (24 * rho_rad_PTsector) * m2factor
|
|
2076
|
+
|
|
2077
|
+
# alpha_hyd is the Giese-KKS strength fed to calc_kappas. Denominator is
|
|
2078
|
+
# 3 * w_sym; w_sym must include the decoupled bath only when the two sectors
|
|
2079
|
+
# are hydrodynamically coupled, otherwise the wall only does work on the PT
|
|
2080
|
+
# sector.
|
|
2081
|
+
include_decoupled_in_enthalpy = bool(pot.config.gwConf.coupled_hydrodynamics)
|
|
2082
|
+
Veff_sym_w = pot.Vtot(high_phi, T, include_decoupled=include_decoupled_in_enthalpy) - V0_ref
|
|
2083
|
+
e_sym_w = pot.energyDensity(high_phi, T, include_decoupled=include_decoupled_in_enthalpy)
|
|
2084
|
+
alpha_hyd = (theta_sym - theta_bro) / (3 * (-Veff_sym_w + e_sym_w))
|
|
2085
|
+
|
|
2086
|
+
return alpha_p, alpha_theta, alpha_thetabar, alpha_e, alpha_hyd, alpha_inf, alpha_eq
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
def calc_betaH_S3_approx(T, outdict, pot, phase_sym, phase_bro, tmin, tmax, verbose=False):
|
|
2090
|
+
"""
|
|
2091
|
+
Calculate the betaH parameter at temperature T
|
|
2092
|
+
using the derivative of the bounce action with respect to
|
|
2093
|
+
the temperature.
|
|
2094
|
+
"""
|
|
2095
|
+
S = calcAction(pot, T, phase_sym, phase_bro, outdict)
|
|
2096
|
+
if np.isinf(S):
|
|
2097
|
+
if verbose:
|
|
2098
|
+
print("Warning: S(T) is inf")
|
|
2099
|
+
return np.nan
|
|
2100
|
+
|
|
2101
|
+
dT = T * 1e-3
|
|
2102
|
+
# if dT is too large, set it to a smaller value
|
|
2103
|
+
if T + dT > tmax:
|
|
2104
|
+
dT = (tmax - T) / 100
|
|
2105
|
+
|
|
2106
|
+
# Try to calculate the derivative to the right of T
|
|
2107
|
+
SdT = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
|
|
2108
|
+
betaH = (SdT - S) / dT - S / T
|
|
2109
|
+
while np.isinf(SdT) and dT / T > 1e-15:
|
|
2110
|
+
if verbose:
|
|
2111
|
+
print("SdT is inf, reducing dT to ", dT, " and trying again")
|
|
2112
|
+
dT *= 0.1
|
|
2113
|
+
SdT = calcAction(pot, T + dT, phase_sym, phase_bro, outdict)
|
|
2114
|
+
betaH = (SdT - S) / dT - S / T
|
|
2115
|
+
|
|
2116
|
+
# if SdT is inf, we cannot calculate betaH
|
|
2117
|
+
# if beta/H is negative, there was a numerical issue
|
|
2118
|
+
if np.isinf(SdT) or betaH < 0:
|
|
2119
|
+
# Try to calculate the derivative to the left of T
|
|
2120
|
+
dT = T * 1e-3
|
|
2121
|
+
if T - dT < tmin:
|
|
2122
|
+
dT = (T - tmin) / 100
|
|
2123
|
+
SdT_ = calcAction(pot, T - dT, phase_sym, phase_bro, outdict)
|
|
2124
|
+
betaH = (S - SdT_) / dT - S / T
|
|
2125
|
+
while np.isinf(SdT_) and dT / T > 1e-15:
|
|
2126
|
+
if verbose:
|
|
2127
|
+
print("SdT is still inf, reducing dT to ", dT, " and trying again")
|
|
2128
|
+
dT *= 0.1
|
|
2129
|
+
SdT_ = calcAction(pot, T - dT, phase_sym, phase_bro, outdict)
|
|
2130
|
+
betaH = (S - SdT_) / dT - S / T
|
|
2131
|
+
if np.isinf(SdT_):
|
|
2132
|
+
if verbose:
|
|
2133
|
+
print("Warning: SdT_ is inf, cannot calculate betaH")
|
|
2134
|
+
return np.nan
|
|
2135
|
+
|
|
2136
|
+
if np.isinf(SdT):
|
|
2137
|
+
if verbose:
|
|
2138
|
+
print("Warning: S(T+dT) is inf, cannot calculate betaH")
|
|
2139
|
+
return np.nan
|
|
2140
|
+
|
|
2141
|
+
if betaH <= 0 and verbose:
|
|
2142
|
+
print("Warning: betaH is negative!")
|
|
2143
|
+
|
|
2144
|
+
return betaH
|
|
2145
|
+
|
|
2146
|
+
|
|
2147
|
+
def calcMeanBubbleSeparation(
|
|
2148
|
+
T,
|
|
2149
|
+
Tmax,
|
|
2150
|
+
Sint,
|
|
2151
|
+
Pint,
|
|
2152
|
+
Hint,
|
|
2153
|
+
entropyInt=None,
|
|
2154
|
+
coolingInt=None,
|
|
2155
|
+
verbose=False,
|
|
2156
|
+
):
|
|
2157
|
+
"""Calculate the mean bubble separation at temperature T.
|
|
2158
|
+
|
|
2159
|
+
Parameters
|
|
2160
|
+
----------
|
|
2161
|
+
T : float
|
|
2162
|
+
Symmetric phase temperature
|
|
2163
|
+
Tmax : float
|
|
2164
|
+
Upper limit of the integral, usually the nucleation temperature
|
|
2165
|
+
phase_sym : scipy.interpolate.interp1d object
|
|
2166
|
+
The symmetric phase
|
|
2167
|
+
Sint : scipy.interpolate.interp1d object
|
|
2168
|
+
The action
|
|
2169
|
+
Pint : scipy.interpolate.interp1d object
|
|
2170
|
+
The true vacuum fraction.
|
|
2171
|
+
Hint : scipy.interpolate.interp1d object
|
|
2172
|
+
The Hubble rate
|
|
2173
|
+
Returns
|
|
2174
|
+
----------
|
|
2175
|
+
R : float
|
|
2176
|
+
Mean bubble separation."""
|
|
2177
|
+
|
|
2178
|
+
# See eq. (5.42) in 2305.02357.
|
|
2179
|
+
if not (np.isfinite(T) and np.isfinite(Tmax)) or Tmax <= T:
|
|
2180
|
+
return np.inf
|
|
2181
|
+
if entropyInt is None and coolingInt is None:
|
|
2182
|
+
# Preserve the bag-mode integration path exactly so RH/betaH_from_RH
|
|
2183
|
+
# remain comparable to fixed-grid reference runs.
|
|
2184
|
+
Tr = np.linspace(T, Tmax, 10_000)
|
|
2185
|
+
else:
|
|
2186
|
+
# For entropy-aware histories, use a relative-temperature grid so the
|
|
2187
|
+
# numerical resolution is stable under overall temperature rescalings.
|
|
2188
|
+
if T > 0.0 and Tmax > 0.0 and Tmax / T > 1.0 + 1e-10:
|
|
2189
|
+
Tr = np.geomspace(T, Tmax, 10_000)
|
|
2190
|
+
else:
|
|
2191
|
+
Tr = np.linspace(T, Tmax, 10_000)
|
|
2192
|
+
entropy_T = max(float(entropyInt(T)), 1e-300)
|
|
2193
|
+
entropy_Tr = np.maximum(np.asarray(entropyInt(Tr), dtype=float), 1e-300)
|
|
2194
|
+
entropy_ratio = np.clip(entropy_T / entropy_Tr, 0.0, np.inf)
|
|
2195
|
+
if entropyInt is None:
|
|
2196
|
+
entropy_ratio = (T / Tr) ** 3
|
|
2197
|
+
if coolingInt is None:
|
|
2198
|
+
cooling_factor = 1.0
|
|
2199
|
+
else:
|
|
2200
|
+
cooling_factor = np.maximum(np.asarray(coolingInt(Tr), dtype=float), 1e-300)
|
|
2201
|
+
integrant = Gamma(Tr, Sint(Tr)) * (1 - Pint(Tr)) / (cooling_factor * Tr * Hint(Tr)) * entropy_ratio
|
|
2202
|
+
integrant[np.isnan(integrant)] = 0 # Replace NaNs (due to infinite action) with 0
|
|
2203
|
+
res = integrate.trapezoid(integrant, x=Tr)
|
|
2204
|
+
res = np.power(res, -1 / 3)
|
|
2205
|
+
return res
|
|
2206
|
+
|
|
2207
|
+
|
|
2208
|
+
def calcTf(Tperc, Tlow, Pint, pot, verbose=False):
|
|
2209
|
+
"""Calculate the final temperature at which the transition ends.
|
|
2210
|
+
|
|
2211
|
+
Parameters
|
|
2212
|
+
----------
|
|
2213
|
+
Tperc : float
|
|
2214
|
+
Symmetric phase percolation temperature, i.e. maximally possible value of the final temperature
|
|
2215
|
+
Tlow : float
|
|
2216
|
+
Lowest temperature considered in the percolation computation. At this temperature, the true vacuum
|
|
2217
|
+
fraction Pint is >= f_final ~ 0.99. The final temperature must thus be <= Tlow.
|
|
2218
|
+
phase_sym : scipy.interpolate.interp1d object
|
|
2219
|
+
The symmetric phase
|
|
2220
|
+
Sint : scipy.interpolate.interp1d object
|
|
2221
|
+
The action
|
|
2222
|
+
Pint : scipy.interpolate.interp1d object
|
|
2223
|
+
The true vacuum fraction.
|
|
2224
|
+
Returns
|
|
2225
|
+
----------
|
|
2226
|
+
Tf : float
|
|
2227
|
+
Final temperature."""
|
|
2228
|
+
|
|
2229
|
+
f_final = pot.config.percolationConf.f_final # true vacuum fraction at final temperature
|
|
2230
|
+
|
|
2231
|
+
# Step 1: Check if the true vacuum fraction at Tlow is >= f_final
|
|
2232
|
+
if Pint(Tlow) < f_final:
|
|
2233
|
+
# It looks like the transition cannot completely finish
|
|
2234
|
+
msg = "The transition cannot completely finish, Pint(Tlow) < f_final: " + str(Pint(Tlow)) + " < " + str(f_final)
|
|
2235
|
+
raise errors.EternalInflationError(msg)
|
|
2236
|
+
|
|
2237
|
+
# Step 2: Find Tf by solving Pint(Tf) = f_final
|
|
2238
|
+
try:
|
|
2239
|
+
Tf = optimize.brentq(lambda TSYM: Pint(TSYM) - f_final, Tlow, Tperc)
|
|
2240
|
+
except Exception as e:
|
|
2241
|
+
msg = "Error in calculating Tf: " + str(e)
|
|
2242
|
+
if verbose:
|
|
2243
|
+
print(msg)
|
|
2244
|
+
raise errors.PercolationError(e)
|
|
2245
|
+
|
|
2246
|
+
# Here, an eternal inflation criterion after Lewicki et al. 1809.08242 eq. (2.26)
|
|
2247
|
+
# could be implemented. We decided against it so far after some consideration
|
|
2248
|
+
# and preliminary tests: dP/dT < -3*(1 - P(T)) /T seems to be fulfilled at
|
|
2249
|
+
# T_final already in all relevant cases.
|
|
2250
|
+
return Tf
|