TransitionListener 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. transitionlistener/__init__.py +21 -0
  2. transitionlistener/bubbledynamics.py +2250 -0
  3. transitionlistener/bubbledynamics_fixedstep.py +2929 -0
  4. transitionlistener/check_installation.py +309 -0
  5. transitionlistener/ckm.py +91 -0
  6. transitionlistener/colors.py +288 -0
  7. transitionlistener/config.py +185 -0
  8. transitionlistener/constants.py +64 -0
  9. transitionlistener/counterterms/__init__.py +230 -0
  10. transitionlistener/counterterms/cw.py +298 -0
  11. transitionlistener/counterterms/tensors.py +343 -0
  12. transitionlistener/counterterms/twohdm.py +520 -0
  13. transitionlistener/counterterms/twohdm_solver.py +374 -0
  14. transitionlistener/errors.py +315 -0
  15. transitionlistener/examples/bp_et.yaml +64 -0
  16. transitionlistener/examples/bp_lisa.yaml +70 -0
  17. transitionlistener/examples/bp_muares.yaml +69 -0
  18. transitionlistener/examples/bp_pta.yaml +64 -0
  19. transitionlistener/examples/example_dark_photon.yaml +73 -0
  20. transitionlistener/examples/example_grid.yaml +55 -0
  21. transitionlistener/examples/example_line.yaml +51 -0
  22. transitionlistener/examples/example_point.yaml +73 -0
  23. transitionlistener/examples/example_random_conformal.yaml +42 -0
  24. transitionlistener/examples/example_ultranest.yaml +39 -0
  25. transitionlistener/finiteT.py +468 -0
  26. transitionlistener/generic_potential.py +1521 -0
  27. transitionlistener/gridplots.py +592 -0
  28. transitionlistener/gwfopt.py +604 -0
  29. transitionlistener/helper_functions.py +843 -0
  30. transitionlistener/hydrodynamics.py +647 -0
  31. transitionlistener/interface/__init__.py +95 -0
  32. transitionlistener/interface/check.py +51 -0
  33. transitionlistener/interface/cli.py +211 -0
  34. transitionlistener/interface/config_loader.py +264 -0
  35. transitionlistener/interface/logging_utils.py +30 -0
  36. transitionlistener/interface/output_schema.py +296 -0
  37. transitionlistener/interface/pipeline.py +300 -0
  38. transitionlistener/interface/samplers.py +565 -0
  39. transitionlistener/interface/scans.py +468 -0
  40. transitionlistener/interface/single_point.py +581 -0
  41. transitionlistener/interface/state.py +61 -0
  42. transitionlistener/lineplots.py +597 -0
  43. transitionlistener/logo/TL-logo_large.png +0 -0
  44. transitionlistener/logo/TL-logo_small.png +0 -0
  45. transitionlistener/models/TL_2HDM.py +512 -0
  46. transitionlistener/models/TL_conformal_dark_u1.py +202 -0
  47. transitionlistener/models/TL_dark_U1.py +198 -0
  48. transitionlistener/models/TL_dark_U1_g_parameterization.py +198 -0
  49. transitionlistener/models/TL_dark_flipflop.py +274 -0
  50. transitionlistener/models/templatePotential.py +260 -0
  51. transitionlistener/nucleation.py +412 -0
  52. transitionlistener/observability.py +790 -0
  53. transitionlistener/particles.py +436 -0
  54. transitionlistener/pathDeformation.py +1462 -0
  55. transitionlistener/percolation_adaptive_gridbuilders.py +1385 -0
  56. transitionlistener/percolation_adaptive_rate.py +1049 -0
  57. transitionlistener/percolation_adaptivestepsize.py +1632 -0
  58. transitionlistener/percolation_fixedstepsize.py +618 -0
  59. transitionlistener/phases.py +1499 -0
  60. transitionlistener/plot_settings.py +121 -0
  61. transitionlistener/plots.py +1781 -0
  62. transitionlistener/runtime_options.py +278 -0
  63. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/bandwidths.npy +0 -0
  64. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/density.npy +0 -0
  65. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/freqs.npy +0 -0
  66. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log.txt +16 -0
  67. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rhogrid.npy +0 -0
  68. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rholabels.txt +30 -0
  69. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/pulsar_list.txt +1 -0
  70. transitionlistener/tab_data/HLV-O2-PLI.csv +82 -0
  71. transitionlistener/tab_data/HLV-O2-noise.csv +83 -0
  72. transitionlistener/tab_data/HLVK-design-PLI.csv +61 -0
  73. transitionlistener/tab_data/HLVK-design-noise.csv +83 -0
  74. transitionlistener/tab_data/NG15_PLI_hc.csv +31 -0
  75. transitionlistener/tab_data/NG15_noise_hc.csv +56 -0
  76. transitionlistener/tab_data/PLI-dataset.csv +327 -0
  77. transitionlistener/tab_data/energydensities.dat +1001 -0
  78. transitionlistener/tab_data/finiteT_b.dat.txt +1999 -0
  79. transitionlistener/tab_data/finiteT_f.dat.txt +1000 -0
  80. transitionlistener/tab_data/geff_SM.dat +10009 -0
  81. transitionlistener/tab_data/muAres_f_Hz_PLI_h2OmegaGW.csv +131 -0
  82. transitionlistener/tab_data/muAres_f_Hz_h2Omega_noise.csv +99 -0
  83. transitionlistener/tab_data/noise-dataset.csv +327 -0
  84. transitionlistener/tab_data/numberdensities.dat +1001 -0
  85. transitionlistener/tab_data/pressures.dat +1001 -0
  86. transitionlistener/thermodynamics.py +686 -0
  87. transitionlistener/transitionObservables.py +1199 -0
  88. transitionlistener/transitionObservables_fixedstep.py +976 -0
  89. transitionlistener/transitions.py +1000 -0
  90. transitionlistener/tunneling1D.py +910 -0
  91. transitionlistener-2.0.0.dist-info/METADATA +817 -0
  92. transitionlistener-2.0.0.dist-info/RECORD +95 -0
  93. transitionlistener-2.0.0.dist-info/WHEEL +4 -0
  94. transitionlistener-2.0.0.dist-info/entry_points.txt +2 -0
  95. transitionlistener-2.0.0.dist-info/licenses/LICENSE.txt +674 -0
@@ -0,0 +1,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