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,1199 @@
1
+ """Compute thermodynamic and gravitational-wave observables for transitions.
2
+
3
+ Part of TransitionListener v2.0
4
+ Documentation: https://tasillo.de/TransitionListener/
5
+
6
+ Authors:
7
+ Jonas Matuszak <jonas.matuszak@kit.edu>
8
+ Carlo Tasillo <carlo.tasillo@ific.uv.es>
9
+ """
10
+
11
+ import numpy as np
12
+ from dataclasses import dataclass
13
+ from typing import Sequence
14
+ from scipy import interpolate, optimize
15
+ from transitionlistener.generic_potential import generic_potential
16
+ from transitionlistener.phases import PhaseInfo
17
+ from transitionlistener import config
18
+ from transitionlistener.transitions import TransitionInfo
19
+ from . import thermodynamics as td
20
+ from . import errors
21
+ from transitionlistener.hydrodynamics import Hydrodynamics, calc_kappas
22
+ from transitionlistener.bubbledynamics import (
23
+ calcPercAndEvolve,
24
+ calcAlphas,
25
+ calc_betaH_S3,
26
+ calc_betaH_S3_approx,
27
+ calcMeanBubbleSeparation,
28
+ calcTf,
29
+ Tb_criterion,
30
+ HubbleParameter,
31
+ energyDensity,
32
+ PercolationDiagnostics,
33
+ )
34
+
35
+ from . import console, print
36
+ import rich
37
+ from rich.panel import Panel
38
+
39
+
40
+ def _extract_instanton_profile_arrays(instanton) -> tuple[np.ndarray, np.ndarray] | tuple[None, None]:
41
+ """Return radius/field arrays from either supported instanton representation.
42
+
43
+ Most tunneling paths return an object with ``instanton.profile1D.R`` and
44
+ ``instanton.profile1D.Phi``. The lightweight WIP tunneling approximation
45
+ instead returns a plain dictionary carrying raw arrays under
46
+ ``{"profile1D": r, "Phi": phis}``. The observable pipeline only needs the
47
+ radius/field samples to estimate the half-maximum bubble radius ``R0``, so
48
+ we normalize both representations here and let callers degrade gracefully
49
+ when neither layout is available.
50
+ """
51
+
52
+ if instanton is None:
53
+ return None, None
54
+
55
+ profile = getattr(instanton, "profile1D", None)
56
+ if profile is not None and hasattr(profile, "R") and hasattr(profile, "Phi"):
57
+ return np.asarray(profile.R, dtype=float), np.asarray(profile.Phi, dtype=float)
58
+
59
+ if isinstance(instanton, dict):
60
+ profile_dict = instanton.get("profile1D")
61
+ if profile_dict is not None and hasattr(profile_dict, "R") and hasattr(profile_dict, "Phi"):
62
+ return np.asarray(profile_dict.R, dtype=float), np.asarray(profile_dict.Phi, dtype=float)
63
+
64
+ if profile_dict is not None and "Phi" in instanton:
65
+ return np.asarray(profile_dict, dtype=float), np.asarray(instanton["Phi"], dtype=float)
66
+
67
+ return None, None
68
+
69
+
70
+ def _extract_instanton_plot_arrays(instanton) -> tuple[np.ndarray, np.ndarray] | tuple[None, None]:
71
+ """Return radius/field-path arrays suitable for rendering bubble profiles.
72
+
73
+ The plotting routines need the physical field path ``Phi(r)`` rather than
74
+ the 1D path coordinate stored in ``profile1D.Phi``. Support both the
75
+ standard full-tunnelling object layout and the lightweight dictionary
76
+ layout used by the adaptive seedless workflow caches.
77
+ """
78
+
79
+ if instanton is None:
80
+ return None, None
81
+
82
+ profile = getattr(instanton, "profile1D", None)
83
+ fields = getattr(instanton, "Phi", None)
84
+ if profile is not None and hasattr(profile, "R") and fields is not None:
85
+ return np.asarray(profile.R, dtype=float), np.asarray(fields, dtype=float)
86
+
87
+ if isinstance(instanton, dict):
88
+ radii = instanton.get("profile1D")
89
+ fields = instanton.get("Phi")
90
+ if radii is None or fields is None:
91
+ return None, None
92
+ if hasattr(radii, "R"):
93
+ radii = radii.R
94
+ return np.asarray(radii, dtype=float), np.asarray(fields, dtype=float)
95
+
96
+ return None, None
97
+
98
+
99
+ def _select_instanton_near_temperature(outdict: dict, temperature: float):
100
+ """Return the instanton stored closest to ``temperature`` if available."""
101
+
102
+ if not isinstance(outdict, dict) or not np.isfinite(temperature):
103
+ return None
104
+
105
+ candidates: list[tuple[float, object]] = []
106
+ for key, entry in outdict.items():
107
+ try:
108
+ temp = float(key)
109
+ except Exception: # noqa: BLE001
110
+ continue
111
+ if not np.isfinite(temp) or not isinstance(entry, dict):
112
+ continue
113
+ instanton = entry.get("instanton")
114
+ if instanton is None:
115
+ continue
116
+ if isinstance(instanton, float) and not np.isfinite(instanton):
117
+ continue
118
+ candidates.append((abs(temp - temperature), instanton))
119
+
120
+ if not candidates:
121
+ return None
122
+ candidates.sort(key=lambda item: item[0])
123
+ return candidates[0][1]
124
+
125
+
126
+ class _BoundedPchipInterpolator:
127
+ """Shape-preserving spline with explicit out-of-range behaviour."""
128
+
129
+ def __init__(
130
+ self,
131
+ x: Sequence[float],
132
+ y: Sequence[float],
133
+ *,
134
+ fill_low: float | None = None,
135
+ fill_high: float | None = None,
136
+ clip: tuple[float, float] | None = None,
137
+ transform: str = "identity",
138
+ ) -> None:
139
+ """Prepare a monotonic spline with optional clipping and endpoint fills."""
140
+ x_arr = np.asarray(x, dtype=float)
141
+ y_arr = np.asarray(y, dtype=float)
142
+ mask = np.isfinite(x_arr) & np.isfinite(y_arr)
143
+ x_arr = x_arr[mask]
144
+ y_arr = y_arr[mask]
145
+
146
+ if x_arr.size < 2:
147
+ raise ValueError("Need at least two finite samples for interpolation.")
148
+
149
+ order = np.argsort(x_arr)
150
+ x_arr = x_arr[order]
151
+ y_arr = y_arr[order]
152
+
153
+ # Remove duplicate temperatures to satisfy the spline requirements.
154
+ keep = np.concatenate(([True], np.diff(x_arr) > 0))
155
+ x_arr = x_arr[keep]
156
+ y_arr = y_arr[keep]
157
+
158
+ if x_arr.size < 2:
159
+ raise ValueError("Need at least two unique temperature samples for interpolation.")
160
+
161
+ self.x = x_arr
162
+ self._transform = transform
163
+ transformed = self._forward_transform(y_arr)
164
+ self._interp = interpolate.PchipInterpolator(x_arr, transformed, extrapolate=False)
165
+ self._fill_low = fill_low
166
+ self._fill_high = fill_high
167
+ self._clip = clip
168
+
169
+ def _forward_transform(self, values: np.ndarray) -> np.ndarray:
170
+ """Map physical values into the interpolation space."""
171
+ arr = np.asarray(values, dtype=float)
172
+ if self._transform == "identity":
173
+ return arr
174
+ if self._transform == "log_false_fraction":
175
+ clipped = np.clip(arr, 0.0, 1.0 - 1e-15)
176
+ return np.log(np.clip(1.0 - clipped, 1e-300, 1.0))
177
+ raise ValueError(f"Unknown spline transform: {self._transform}")
178
+
179
+ def _inverse_transform(self, values: np.ndarray) -> np.ndarray:
180
+ """Map interpolated values back to the physical space."""
181
+ arr = np.asarray(values, dtype=float)
182
+ if self._transform == "identity":
183
+ return arr
184
+ if self._transform == "log_false_fraction":
185
+ false_fraction = np.exp(np.clip(arr, -700.0, 0.0))
186
+ return 1.0 - false_fraction
187
+ raise ValueError(f"Unknown spline transform: {self._transform}")
188
+
189
+ def __call__(self, values):
190
+ """Evaluate the spline while honouring the configured fill and clip rules."""
191
+ arr = np.asarray(values, dtype=float)
192
+ out = self._inverse_transform(np.asarray(self._interp(arr), dtype=float))
193
+
194
+ if self._fill_low is not None:
195
+ out = np.where(arr < self.x[0], self._fill_low, out)
196
+ if self._fill_high is not None:
197
+ out = np.where(arr > self.x[-1], self._fill_high, out)
198
+ if self._clip is not None:
199
+ out = np.clip(out, self._clip[0], self._clip[1])
200
+
201
+ if np.isscalar(values):
202
+ return float(np.asarray(out).reshape(()))
203
+ return out
204
+
205
+
206
+ @dataclass
207
+ class TransitionObservableInfo:
208
+ """Collection of transition observable data."""
209
+ v_wall: float = np.nan
210
+ c_s: float = np.nan
211
+ c_s_sym: float = np.nan
212
+ c_s_bro: float = np.nan
213
+ Tperc: float = np.nan
214
+ Tperc_SM_GeV: float = np.nan
215
+ Tcrit_SM_GeV: float = np.nan
216
+ xi_crit: float = np.nan
217
+ Tnuc_SM_GeV: float = np.nan
218
+ Treh: float = np.nan
219
+ Treh_SM_GeV: float = np.nan
220
+ alpha: float = np.nan
221
+ alpha_theta: float = np.nan
222
+ alpha_thetabar: float = np.nan
223
+ alpha_p: float = np.nan
224
+ alpha_e: float = np.nan
225
+ alpha_hyd: float = np.nan
226
+ alpha_inf: float = np.nan
227
+ alpha_eq: float = np.nan
228
+ RH: float = np.nan
229
+ g_eff_tot_reh: float = np.nan
230
+ h_eff_tot_reh: float = np.nan
231
+ g0: float = np.nan
232
+ h0: float = np.nan
233
+ betaH_S3: float = np.nan
234
+ betaH_RH: float = np.nan
235
+ Tf_SM_GeV: float = np.nan
236
+ kappa_phi: float = np.nan
237
+ kappa_sw: float = np.nan
238
+ kappa_turb: float = np.nan
239
+ D: float = np.nan
240
+ step: float = np.nan
241
+ total_steps: float = np.nan
242
+ WARNING_too_weak_to_compute_perc: bool = False
243
+ WARNING_no_perc_splines: bool = False
244
+ WARNING_betaH_small: bool = False
245
+ WARNING_betaH_very_small: bool = False
246
+ WARNING_betaH_mismatch: bool = False
247
+ WARNING_betaH_nonfinite: bool = False
248
+ WARNING_nucleationRate_nonexponential: bool = False
249
+ WARNING_not_T0_global_min: bool = False
250
+
251
+
252
+ @dataclass
253
+ class TransitionContext:
254
+ """Collect shared inputs and outputs for a single transition."""
255
+
256
+ tr: TransitionInfo
257
+ pot: generic_potential
258
+ phases: list[PhaseInfo]
259
+ GWconfig: config.GWConf
260
+ PercolationConf: config.PercolationConf
261
+ derived_param_names: list[str]
262
+ verbose: bool
263
+ derived_params: dict
264
+ Tnuc: float | None
265
+ outdict: dict
266
+ phase_symmetric: PhaseInfo
267
+ phase_broken: PhaseInfo
268
+
269
+
270
+ @dataclass
271
+ class PercolationResult:
272
+ """Hold percolation-related outputs and splines when available."""
273
+
274
+ Tperc: float
275
+ TSYM: np.ndarray | None
276
+ H: np.ndarray | None
277
+ P: np.ndarray | None
278
+ TBRO: np.ndarray | None
279
+ S: np.ndarray | None
280
+ Pint: interpolate.interp1d | None
281
+ Sint: interpolate.interp1d | None
282
+ Hint: interpolate.interp1d | None
283
+ entropyInt: interpolate.interp1d | None
284
+ coolingInt: interpolate.interp1d | None
285
+ metadata: PercolationDiagnostics | None
286
+ successful: bool
287
+
288
+
289
+ class TransitionObservables:
290
+ """Evaluate derived observables for every transition of a potential."""
291
+
292
+ def __init__(self, pot: generic_potential, phases: list[PhaseInfo],
293
+ transitions: list[TransitionInfo], verbose=False):
294
+ """Store inputs and immediately compute all derived quantities."""
295
+ self.pot = pot
296
+ self.phases = phases
297
+ self.GWconfig = pot.config.gwConf
298
+ self.PercolationConf = pot.config.percolationConf
299
+ self.transitions = transitions
300
+ self.verbose = verbose
301
+ self.derived_param_names = [name for name in config.all_observables.keys()]
302
+
303
+ if verbose:
304
+ console.print(
305
+ "[green bold]\nStart percolation computation...[/]",
306
+ )
307
+
308
+ self.transitionObservables = self.computeTransitionObservables()
309
+ self.check_T0_global_min()
310
+
311
+ def check_T0_global_min(self):
312
+ """Check whether the global minimum at T = Tmin is the last minimum.
313
+ If not, raise an error.
314
+ """
315
+ T0 = self.pot.Tmin
316
+ last_vev = self.phases[self.transitions[-1].low_phase].valAt(self.pot.Tmin)
317
+
318
+ if not np.allclose(last_vev, self.pot.X0):
319
+ # The global minimum at T = 0 is not the last minimum!
320
+
321
+ for i, tr in enumerate(self.transitions):
322
+ if tr is not None and tr.type == 1:
323
+ self.transitions[i].derived_params["WARNING:not_T0_global_min"] = True
324
+ self.transitionObservables[i]["WARNING:not_T0_global_min"] = True
325
+
326
+ msg = (
327
+ "The global minimum at T = {} is not the last minimum found "
328
+ "by the phase tracing algorithm. This may be unwanted, for "
329
+ "example if the potential is expected to have a unique "
330
+ "global minimum at T = {} or when identifying the last phase "
331
+ "with the electroweak vacuum."
332
+ ).format(T0 * self.pot.conversionFactor, T0 * self.pot.conversionFactor)
333
+
334
+ if self.GWconfig.check_if_T0_global_min:
335
+ console.print(f"[bold red]ERROR:[/bold red] {msg}")
336
+ raise errors.GlobalMinimumError(msg)
337
+ elif self.verbose:
338
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
339
+ console.print(
340
+ "[bold yellow]WARNING:[/bold yellow] "
341
+ "Continuing, because the check_if_T0_global_min flag is "
342
+ "set to False."
343
+ )
344
+ else:
345
+ # The global minimum at T = 0 is the last minimum.
346
+ for i, tr in enumerate(self.transitions):
347
+ if tr is not None and tr.type == 1:
348
+ self.transitions[i].derived_params["WARNING:not_T0_global_min"] = False
349
+ self.transitionObservables[i]["WARNING:not_T0_global_min"] = False
350
+
351
+ if self.verbose:
352
+ msg = (
353
+ "The global minimum at T_min is the last minimum found "
354
+ "by the phase tracing algorithm."
355
+ )
356
+ console.print(
357
+ f"[bold cyan]INFO:[/bold cyan] {msg}"
358
+ )
359
+
360
+ def computeTransitionObservables(self):
361
+ """
362
+ Calculate the GW observables for each transition in the
363
+ transitions_dict. This is done by calling the
364
+ `computeTransitionObservables_one_transition`
365
+ function from the `bubbledynamics` module.
366
+
367
+ Raises
368
+ ------
369
+ err
370
+ Raises an error if GW observables cannot be calculated.
371
+
372
+ Returns
373
+ -------
374
+ transitionObservables: dict
375
+ A dictionary with the GW observables for each transition.
376
+ """
377
+ self.transitionObservables = {i: {} for i in range(len(self.transitions))}
378
+ self.percolationResults = {i: None for i in range(len(self.transitions))}
379
+ self.outdicts = {i: None for i in range(len(self.transitions))}
380
+
381
+ for i, tr in enumerate(self.transitions):
382
+ if tr is not None and tr.type == 1:
383
+ try:
384
+ derived_params_dict_tr, percol, outdict = self.computeTransitionObservables_one_transition(
385
+ tr, nAction=self.PercolationConf.n_action
386
+ )
387
+ self.transitions[i].derived_params = derived_params_dict_tr
388
+ self.percolationResults[i] = percol
389
+ self.outdicts[i] = outdict
390
+ self.transitionObservables[i] = derived_params_dict_tr
391
+ except Exception as err:
392
+ if self.verbose:
393
+ print("Error calculating GW parameters: ", err)
394
+ raise err
395
+ return self.transitionObservables
396
+
397
+ def _initialize_transition_context(self, tr: TransitionInfo) -> TransitionContext:
398
+ """Prepare reusable data structures for a single transition."""
399
+ derived_params = {
400
+ "WARNING:too_weak_to_compute_perc": False,
401
+ "WARNING:no_perc_splines": False,
402
+ "WARNING:betaH_small": False,
403
+ "WARNING:betaH_very_small": False,
404
+ "WARNING:betaH_mismatch": False,
405
+ "WARNING:betaH_nonfinite": False,
406
+ "WARNING:nucleationRate_nonexponential": False,
407
+ "WARNING:spline_tnuc_unavailable": False,
408
+ "WARNING:spline_tnuc_not_reached": False,
409
+ "WARNING:spline_tnuc_failed": False,
410
+ }
411
+ seed_temperature = tr.Tnuc
412
+ use_seedless_cache = seed_temperature is None or not np.isfinite(seed_temperature)
413
+ return TransitionContext(
414
+ tr=tr,
415
+ pot=self.pot,
416
+ phases=self.phases,
417
+ GWconfig=self.GWconfig,
418
+ PercolationConf=self.PercolationConf,
419
+ derived_param_names=self.derived_param_names,
420
+ verbose=self.verbose,
421
+ derived_params=derived_params,
422
+ Tnuc=seed_temperature,
423
+ outdict={} if use_seedless_cache else tr.full_tunneling_info,
424
+ phase_symmetric=self.phases[tr.high_phase],
425
+ phase_broken=self.phases[tr.low_phase],
426
+ )
427
+
428
+ def _ensure_wall_velocity(self, ctx: TransitionContext, temperature: float) -> None:
429
+ """Populate v_wall if requested by configuration at one milestone temperature.
430
+
431
+ Accepts ``"LTE"``, ``"c"``, or a numeric value in ``(0, 1]``.
432
+ """
433
+ if "v_wall" not in ctx.derived_param_names:
434
+ return
435
+ GWconfig = ctx.GWconfig
436
+ derived = ctx.derived_params
437
+ if GWconfig.wall_velocity == "LTE":
438
+ hydr = Hydrodynamics(ctx.pot, ctx.phase_symmetric, ctx.phase_broken, ctx.verbose)
439
+ derived["v_wall"] = hydr.calcWallVelocityLTE(temperature)
440
+ elif GWconfig.wall_velocity == "c":
441
+ derived["v_wall"] = 1.0
442
+ elif GWconfig.wall_velocity == "WallGo":
443
+ raise NotImplementedError("WallGo is not implemented yet.")
444
+ else:
445
+ try:
446
+ vw = float(GWconfig.wall_velocity)
447
+ except (TypeError, ValueError):
448
+ raise ValueError(
449
+ "wall_velocity must be 'LTE', 'c', or a float in (0, 1]; "
450
+ f"got {GWconfig.wall_velocity!r}."
451
+ )
452
+ if vw <= 0.0 or vw > 1.0:
453
+ raise ValueError(
454
+ f"wall_velocity must satisfy 0 < vw <= 1; got {vw}."
455
+ )
456
+ derived["v_wall"] = vw
457
+
458
+ def _ensure_sound_speed(self, ctx: TransitionContext, temperature: float) -> None:
459
+ """Populate sound speed values when requested at one milestone temperature."""
460
+ if "c_s" not in ctx.derived_param_names:
461
+ return
462
+ derived = ctx.derived_params
463
+ GWconfig = ctx.GWconfig
464
+ if ctx.verbose:
465
+ print("Calculating sound speed...")
466
+ if GWconfig.sound_speed == "compute":
467
+ hydr = Hydrodynamics(ctx.pot, ctx.phase_symmetric, ctx.phase_broken, ctx.verbose)
468
+ c_sb = hydr.calc_cs(temperature, sym=False)
469
+ c_ss = hydr.calc_cs(temperature, sym=True)
470
+ derived["c_s"] = c_sb
471
+ derived["c_s_sym"] = c_ss
472
+ derived["c_s_bro"] = c_sb
473
+ elif GWconfig.sound_speed == "1/3":
474
+ value = 1 / np.sqrt(3)
475
+ derived["c_s"] = value
476
+ derived["c_s_sym"] = value
477
+ derived["c_s_bro"] = value
478
+ else:
479
+ raise NotImplementedError(
480
+ "The GWconfig.sound_speed option is not implemented yet: "
481
+ + GWconfig.sound_speed
482
+ )
483
+
484
+ def _compute_transition_strength(self, ctx: TransitionContext, temperature: float) -> float:
485
+ """Return alpha_theta at the supplied milestone temperature."""
486
+ _, alpha_theta, _, _, _, _, _ = calcAlphas(
487
+ temperature, ctx.pot, ctx.phase_symmetric, ctx.phase_broken,
488
+ verbose=ctx.verbose,
489
+ )
490
+ if ctx.verbose:
491
+ print("alpha at milestone temperature =", np.squeeze(alpha_theta))
492
+ return np.squeeze(alpha_theta)
493
+
494
+ def _compute_percolation(
495
+ self,
496
+ ctx: TransitionContext,
497
+ rtol: float,
498
+ nAction: int,
499
+ ) -> PercolationResult:
500
+ """Perform the percolation computation and build the associated splines."""
501
+ pot = ctx.pot
502
+ derived = ctx.derived_params
503
+ verbose = ctx.verbose
504
+
505
+ try:
506
+ Tperc, TSYM, H, P, TBRO, S, metadata = calcPercAndEvolve(
507
+ ctx.outdict,
508
+ ctx.Tnuc,
509
+ ctx.phase_symmetric,
510
+ ctx.phase_broken,
511
+ pot,
512
+ rtol=rtol,
513
+ nAction=nAction,
514
+ verbose=verbose,
515
+ vw=ctx.derived_params.get("v_wall", 1.0),
516
+ return_metadata=True,
517
+ )
518
+ except Exception as err:
519
+ if verbose:
520
+ print("Fatal error in calcPercAndEvolve: ", err)
521
+ raise
522
+
523
+ if verbose:
524
+ print("Calculating percolation splines...")
525
+ Pint = Sint = Hint = entropyInt = coolingInt = None
526
+ core_spline_error = None
527
+ try:
528
+ Pint = _BoundedPchipInterpolator(
529
+ TSYM,
530
+ P,
531
+ fill_low=1.0,
532
+ fill_high=0.0,
533
+ clip=(0.0, 1.0),
534
+ )
535
+ except Exception as err:
536
+ core_spline_error = err
537
+
538
+ try:
539
+ finite_s = S[np.isfinite(S)]
540
+ Sint = _BoundedPchipInterpolator(
541
+ TSYM,
542
+ S,
543
+ fill_low=float(finite_s[-1]),
544
+ fill_high=float(finite_s[0]),
545
+ )
546
+ except Exception as err:
547
+ if core_spline_error is None:
548
+ core_spline_error = err
549
+
550
+ try:
551
+ finite_h = H[np.isfinite(H)]
552
+ Hint = _BoundedPchipInterpolator(
553
+ TSYM,
554
+ H,
555
+ fill_low=float(finite_h[-1]),
556
+ fill_high=float(finite_h[0]),
557
+ )
558
+ except Exception as err:
559
+ if core_spline_error is None:
560
+ core_spline_error = err
561
+
562
+ if core_spline_error is not None:
563
+ derived["WARNING:no_perc_splines"] = True
564
+ if verbose:
565
+ print("Error in computing percolation splines: ", core_spline_error)
566
+ msg = (
567
+ "WARNING: Cannot calculate the splines. Continue with the "
568
+ "percolation temperature, but no splines will be available "
569
+ "for further calculations."
570
+ )
571
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
572
+ Pint = Sint = Hint = entropyInt = coolingInt = None
573
+
574
+ return PercolationResult(
575
+ Tperc=Tperc,
576
+ TSYM=TSYM,
577
+ H=H,
578
+ P=P,
579
+ TBRO=TBRO,
580
+ S=S,
581
+ Pint=Pint,
582
+ Sint=Sint,
583
+ Hint=Hint,
584
+ entropyInt=entropyInt,
585
+ coolingInt=coolingInt,
586
+ metadata=metadata,
587
+ successful=True,
588
+ )
589
+
590
+ def _populate_temperature_observables(
591
+ self,
592
+ ctx: TransitionContext,
593
+ percolation: PercolationResult,
594
+ ) -> None:
595
+ """Fill temperature-related entries in the derived parameters."""
596
+ derived = ctx.derived_params
597
+ pot = ctx.pot
598
+
599
+ if "Tperc_SM_GeV" in ctx.derived_param_names:
600
+ if ctx.verbose:
601
+ print("Calculating percolation temperature...")
602
+ derived["Tperc"] = percolation.Tperc
603
+ derived["Tperc_SM_GeV"] = percolation.Tperc * pot.conversionFactor
604
+
605
+ if "Tcrit_SM_GeV" in ctx.derived_param_names:
606
+ if ctx.verbose:
607
+ print("Calculating critical temperature...")
608
+ derived["Tcrit_SM_GeV"] = ctx.tr.Tcrit * pot.conversionFactor
609
+
610
+ if "xi_crit" in ctx.derived_param_names:
611
+ if ctx.verbose:
612
+ print("Calculating critical vev over temperature...")
613
+ T_crit = ctx.tr.Tcrit
614
+ vev_crit = np.sqrt(np.sum(ctx.phase_broken.valAt(T_crit) ** 2))
615
+ derived["xi_crit"] = vev_crit / T_crit
616
+
617
+ if "Tnuc_SM_GeV" in ctx.derived_param_names:
618
+ if ctx.verbose:
619
+ print("Calculating nucleation temperature...")
620
+ metadata_tnuc = None
621
+ if percolation.metadata is not None:
622
+ metadata_tnuc = getattr(percolation.metadata, "spline_tnuc", None)
623
+ warning = getattr(percolation.metadata, "spline_tnuc_warning", None)
624
+ if warning is not None:
625
+ warning_key = f"WARNING:{warning}"
626
+ derived[warning_key] = True
627
+ if ctx.tr.Tnuc is not None and np.isfinite(ctx.tr.Tnuc):
628
+ derived["Tnuc_SM_GeV"] = ctx.tr.Tnuc * pot.conversionFactor
629
+ elif metadata_tnuc is not None and np.isfinite(metadata_tnuc):
630
+ derived["Tnuc_SM_GeV"] = float(metadata_tnuc) * pot.conversionFactor
631
+ else:
632
+ derived["Tnuc_SM_GeV"] = np.nan
633
+
634
+ if "Treh_SM_GeV" in ctx.derived_param_names:
635
+ if ctx.verbose:
636
+ print("Calculating reheating temperature...")
637
+ try:
638
+ Treh = optimize.brentq(
639
+ Tb_criterion,
640
+ float(ctx.phase_broken.Tmin),
641
+ float(ctx.phase_broken.Tmax),
642
+ args=(
643
+ percolation.Tperc,
644
+ ctx.phase_symmetric,
645
+ ctx.phase_broken,
646
+ pot,
647
+ ),
648
+ )
649
+ except ValueError:
650
+ if ctx.verbose:
651
+ msg = (
652
+ "WARNING: Tb_criterion does not bracket a root in the "
653
+ "broken-phase temperature range; falling back to Treh = Tperc."
654
+ )
655
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
656
+ Treh = percolation.Tperc
657
+ derived["Treh"] = Treh
658
+ derived["Treh_SM_GeV"] = Treh * pot.conversionFactor
659
+
660
+ def _compute_alpha_parameters(
661
+ self,
662
+ ctx: TransitionContext,
663
+ percolation: PercolationResult,
664
+ ) -> None:
665
+ """Compute alpha-related quantities if requested."""
666
+ if "alpha" not in ctx.derived_param_names:
667
+ return
668
+ if ctx.verbose:
669
+ print("Calculating alpha parameters...")
670
+ alpha_p, alpha_theta, alpha_thetabar, alpha_e, alpha_hyd, alpha_inf, alpha_eq = calcAlphas(
671
+ percolation.Tperc, ctx.pot, ctx.phase_symmetric, ctx.phase_broken,
672
+ verbose=ctx.verbose,
673
+ )
674
+ derived = ctx.derived_params
675
+ derived["alpha"] = np.squeeze(alpha_thetabar)
676
+ derived["alpha_theta"] = np.squeeze(alpha_theta)
677
+ derived["alpha_thetabar"] = np.squeeze(alpha_thetabar)
678
+ derived["alpha_p"] = np.squeeze(alpha_p)
679
+ derived["alpha_e"] = np.squeeze(alpha_e)
680
+ derived["alpha_hyd"] = np.squeeze(alpha_hyd)
681
+ derived["alpha_inf"] = np.squeeze(alpha_inf)
682
+ derived["alpha_eq"] = np.squeeze(alpha_eq)
683
+
684
+ def _compute_beta_and_separation(
685
+ self,
686
+ ctx: TransitionContext,
687
+ percolation: PercolationResult,
688
+ tmin: float,
689
+ tmax: float,
690
+ ) -> None:
691
+ """Populate beta/H variants, RH and Tf if configured."""
692
+ derived = ctx.derived_params
693
+ pot = ctx.pot
694
+ verbose = ctx.verbose
695
+ outdict = ctx.outdict
696
+ phase_symmetric = ctx.phase_symmetric
697
+ phase_broken = ctx.phase_broken
698
+ beta_time_factor = self._beta_time_temperature_factor(ctx, percolation.Tperc)
699
+
700
+ if "betaH_S3_approx" in ctx.derived_param_names:
701
+ if verbose:
702
+ print("Calculating beta/H (approximation through derivative of bounce action)...")
703
+ derived["betaH_S3_approx"] = calc_betaH_S3_approx(
704
+ percolation.Tperc, outdict, pot, phase_symmetric, phase_broken, tmin, tmax, verbose=verbose
705
+ )
706
+ if np.isfinite(derived.get("betaH_S3_approx", np.nan)):
707
+ derived["betaH_S3_approx"] *= beta_time_factor
708
+
709
+ if "RH" in ctx.derived_param_names:
710
+ if verbose:
711
+ print("Calculating mean bubble separation...")
712
+ if (
713
+ derived.get("WARNING:no_perc_splines", False)
714
+ or percolation.Sint is None
715
+ or percolation.TSYM is None
716
+ ):
717
+ if verbose:
718
+ msg = (
719
+ "WARNING: Set RH to NaN "
720
+ "because no percolation splines were found."
721
+ )
722
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
723
+ derived["RH"] = np.nan
724
+ else:
725
+ support_tmax = float(np.nanmax(percolation.TSYM))
726
+ tmax_rh = support_tmax
727
+ metadata_tnuc = (
728
+ None
729
+ if percolation.metadata is None
730
+ else getattr(percolation.metadata, "spline_tnuc", None)
731
+ )
732
+ for tnuc_candidate in (ctx.tr.Tnuc, metadata_tnuc):
733
+ if (
734
+ tnuc_candidate is not None
735
+ and np.isfinite(tnuc_candidate)
736
+ and float(tnuc_candidate) > percolation.Tperc
737
+ ):
738
+ tmax_rh = min(float(tnuc_candidate), support_tmax)
739
+ break
740
+ Rsep = calcMeanBubbleSeparation(
741
+ percolation.Tperc,
742
+ tmax_rh,
743
+ percolation.Sint,
744
+ percolation.Pint,
745
+ percolation.Hint,
746
+ percolation.entropyInt,
747
+ percolation.coolingInt,
748
+ verbose=verbose,
749
+ )
750
+ RsepHperc = Rsep * percolation.Hint(percolation.Tperc)
751
+ derived["RH"] = RsepHperc
752
+
753
+ # This combines the dark-sector contribution with the SM bath and
754
+ # therefore assumes a DS + SM setup.
755
+ if "g_eff_tot_reh" in ctx.derived_param_names:
756
+ if verbose:
757
+ print("Calculating g_eff_tot_reh...")
758
+ bosons = pot.boson_massSq(ctx.phase_broken.valAt(percolation.Tperc), percolation.Tperc)
759
+ fermions = pot.fermion_massSq(ctx.phase_broken.valAt(percolation.Tperc))
760
+ gefftotreh = td.e_geffDS(
761
+ bosons*(~pot.mass_spectrum.is_SM_bosons),
762
+ fermions*(~pot.mass_spectrum.is_SM_fermions),
763
+ percolation.Tperc,
764
+ ) + td.e_geffSM(percolation.Tperc, pot.conversionFactor)
765
+ derived["g_eff_tot_reh"] = gefftotreh
766
+
767
+ # This combines the dark-sector contribution with the SM bath and
768
+ # therefore assumes a DS + SM setup.
769
+ if "h_eff_tot_reh" in ctx.derived_param_names:
770
+ if verbose:
771
+ print("Calculating h_eff_tot_reh...")
772
+ bosons = pot.boson_massSq(ctx.phase_broken.valAt(percolation.Tperc), percolation.Tperc)
773
+ fermions = pot.fermion_massSq(ctx.phase_broken.valAt(percolation.Tperc))
774
+ hefftotreh = td.s_geffDS(
775
+ bosons*(~pot.mass_spectrum.is_SM_bosons),
776
+ fermions*(~pot.mass_spectrum.is_SM_fermions),
777
+ percolation.Tperc,
778
+ ) + td.s_geffSM(percolation.Tperc, pot.conversionFactor)
779
+ derived["h_eff_tot_reh"] = hefftotreh
780
+
781
+ if "g0" in ctx.derived_param_names:
782
+ derived["g0"] = 2
783
+
784
+ if "h0" in ctx.derived_param_names:
785
+ derived["h0"] = 3.91
786
+
787
+ if (
788
+ "betaH_S3" in ctx.derived_param_names
789
+ or "betaH_RH" in ctx.derived_param_names
790
+ or "Tf_SM_GeV" in ctx.derived_param_names
791
+ ):
792
+ self._populate_beta_related(
793
+ ctx,
794
+ percolation,
795
+ tmin,
796
+ tmax,
797
+ )
798
+
799
+ def _populate_beta_related(
800
+ self,
801
+ ctx: TransitionContext,
802
+ percolation: PercolationResult,
803
+ tmin: float,
804
+ tmax: float,
805
+ ) -> None:
806
+ """Gather betaH variants and Tf once percolation data is available."""
807
+ derived = ctx.derived_params
808
+ verbose = ctx.verbose
809
+ pot = ctx.pot
810
+ beta_time_factor = self._beta_time_temperature_factor(ctx, percolation.Tperc)
811
+
812
+ if "betaH_S3" in ctx.derived_param_names:
813
+ if verbose:
814
+ print("Calculating betaH_S3 (through the percolation splines)...")
815
+ if derived.get("WARNING:no_perc_splines", False) or percolation.Sint is None:
816
+ if verbose:
817
+ msg = (
818
+ "WARNING: Approximate betaH_S3 only by using betaH_approx "
819
+ "because no percolation splines were found."
820
+ )
821
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
822
+
823
+ derived["betaH_S3"] = calc_betaH_S3_approx(
824
+ percolation.Tperc,
825
+ ctx.outdict,
826
+ ctx.pot,
827
+ ctx.phase_symmetric,
828
+ ctx.phase_broken,
829
+ tmin,
830
+ tmax,
831
+ verbose=verbose,
832
+ )
833
+ else:
834
+ derived["betaH_S3"] = calc_betaH_S3(
835
+ percolation.Tperc,
836
+ percolation.Sint,
837
+ ctx.outdict,
838
+ ctx.pot,
839
+ ctx.phase_symmetric,
840
+ ctx.phase_broken,
841
+ verbose,
842
+ )
843
+ if np.isfinite(derived.get("betaH_S3", np.nan)):
844
+ derived["betaH_S3"] *= beta_time_factor
845
+
846
+ if "betaH_RH" in ctx.derived_param_names:
847
+ RH = derived.get("RH", np.nan)
848
+ if not np.isfinite(RH) or RH <= 0.0:
849
+ if verbose:
850
+ msg = (
851
+ "Set betaH_RH to np.nan "
852
+ "because RH is non-positive or non-finite."
853
+ )
854
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
855
+ derived["betaH_RH"] = np.nan
856
+ else:
857
+ f_perc = ctx.PercolationConf.f_perc
858
+ derived["betaH_RH"] = (
859
+ (8 * np.pi) ** (1 / 3)
860
+ / RH
861
+ * max(derived.get("v_wall", 1.0), derived.get("c_s", 1 / np.sqrt(3)))
862
+ * (1 / f_perc) ** (1 / 3)
863
+ )
864
+
865
+ if "Tf_SM_GeV" in ctx.derived_param_names:
866
+ if verbose:
867
+ print("Calculating Tf_SM_GeV...")
868
+ if (
869
+ derived.get("WARNING:no_perc_splines", False)
870
+ or percolation.Pint is None
871
+ or percolation.TSYM is None
872
+ ):
873
+ if verbose:
874
+ msg = (
875
+ "Set Tf_SM_GeV to np.nan because no "
876
+ "percolation splines were found."
877
+ )
878
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
879
+ Tf_val = np.nan
880
+ else:
881
+ try:
882
+ Tf_val = calcTf(
883
+ percolation.Tperc,
884
+ percolation.TSYM[-1],
885
+ percolation.Pint,
886
+ pot,
887
+ verbose=verbose,
888
+ )
889
+ except errors.PercolationError as err:
890
+ if verbose:
891
+ msg = (
892
+ "Could not compute the temperature at which the "
893
+ "transition ends (Tf_SM_GeV). Set it to np.nan instead."
894
+ )
895
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
896
+ console.print("Error message: ", err)
897
+ Tf_val = np.nan
898
+ derived["Tf_SM_GeV"] = Tf_val * pot.conversionFactor if np.isfinite(Tf_val) else np.nan
899
+
900
+ def _beta_time_temperature_factor(self, ctx: TransitionContext, temperature: float) -> float:
901
+ """Return the factor converting temperature derivatives to beta/H.
902
+
903
+ ``calc_betaH_S3`` evaluates the bag-limit expression
904
+ ``T d(S_3/T)/dT``. For the generalized time-temperature relation,
905
+ ``dT/dt = -3 c_s^2 H T``, the physical beta/H obtains an extra
906
+ factor ``3 c_s^2`` evaluated in the symmetric phase.
907
+ """
908
+ if str(ctx.PercolationConf.time_temperature_mode) != "sound_speed":
909
+ return 1.0
910
+ c_s_sym = ctx.derived_params.get("c_s_sym")
911
+ if c_s_sym is None or not np.isfinite(c_s_sym):
912
+ hydr = Hydrodynamics(ctx.pot, ctx.phase_symmetric, ctx.phase_broken, ctx.verbose)
913
+ c_s_sym = hydr.calc_cs(temperature, sym=True)
914
+ return float(3.0 * c_s_sym * c_s_sym)
915
+
916
+ def _compute_kappa_parameters(
917
+ self,
918
+ ctx: TransitionContext,
919
+ percolation: PercolationResult
920
+ ) -> None:
921
+ """Calculate kappa parameters if required.
922
+
923
+ Parameters
924
+ ----------
925
+ ctx : TransitionContext
926
+ percolation : PercolationResult
927
+
928
+ Returns
929
+ -------
930
+ None.
931
+ """
932
+ names = ctx.derived_param_names
933
+ if not {"kappa_phi", "kappa_sw", "kappa_turb"} & set(names):
934
+ return
935
+ if ctx.verbose:
936
+ print("Calculating kappa parameters...")
937
+ _, alpha_thetaP, _, _, alpha_hydP, alpha_infP, alpha_eqP = calcAlphas(
938
+ percolation.Tperc, ctx.pot, ctx.phase_symmetric, ctx.phase_broken,
939
+ verbose=ctx.verbose,
940
+ )
941
+ # Compute the initial bubble radius R0. We take it to be
942
+ # the radius of the bubble at the nucleation temperature,
943
+ # which is the radius at which the field is halfway
944
+ # between the start and endpoint of the transition. Note
945
+ # that the endpoint of the transition is not necessarily
946
+ # the broken phase minimum, rather the point from which on
947
+ # the field just rolls down to the broken phase minimum, i.e.
948
+ # the endpoint in the instanton solution.
949
+
950
+ # The fixed_step_size solver historically uses the nucleation instanton.
951
+ # In the adaptive_step_size workflow there is no trustworthy Tnuc seed,
952
+ # so use the instanton at Tperc when present, otherwise the closest
953
+ # outdict instanton to Tperc. This keeps R0 tied to the actual
954
+ # percolation solution used for the GW spectrum instead of falling
955
+ # back to NaN.
956
+ instanton = _select_instanton_near_temperature(ctx.outdict, percolation.Tperc)
957
+ if instanton is None:
958
+ instanton = ctx.tr.instanton
959
+ R_, phi_ = _extract_instanton_profile_arrays(instanton)
960
+ if (
961
+ R_ is None
962
+ or phi_ is None
963
+ or R_.size == 0
964
+ or phi_.size == 0
965
+ or R_.size != phi_.size
966
+ or not np.all(np.isfinite(R_))
967
+ or not np.all(np.isfinite(phi_))
968
+ ):
969
+ R0 = np.nan
970
+ else:
971
+ phi_middle = (phi_[0] + phi_[-1]) / 2
972
+ idx = np.argmin(np.abs(phi_ - phi_middle))
973
+
974
+ # this is the radius at which the field is halfway between the
975
+ # start and endpoint of the transition
976
+ R0 = R_[idx]
977
+
978
+ RH = ctx.derived_params.get("RH")
979
+ if RH is None or not np.isfinite(RH) or RH <= 0:
980
+ overlap_tmin = max(float(ctx.phase_symmetric.Tmin), float(ctx.phase_broken.Tmin))
981
+ overlap_tmax = min(float(ctx.phase_symmetric.Tmax), float(ctx.phase_broken.Tmax))
982
+ betaH_approx = calc_betaH_S3_approx(
983
+ percolation.Tperc,
984
+ ctx.outdict,
985
+ ctx.pot,
986
+ ctx.phase_symmetric,
987
+ ctx.phase_broken,
988
+ overlap_tmin,
989
+ overlap_tmax,
990
+ verbose=ctx.verbose,
991
+ )
992
+ if np.isfinite(betaH_approx) and betaH_approx > 0:
993
+ f_perc = ctx.PercolationConf.f_perc
994
+ RH = (8 * np.pi / f_perc) ** (1 / 3) / betaH_approx * max(
995
+ ctx.derived_params.get("v_wall", 1.0), ctx.derived_params.get("c_s", 1 / np.sqrt(3))
996
+ )
997
+ else:
998
+ RH = np.nan
999
+
1000
+ # Bubble size at time of collision (percolation)
1001
+ Hstar = HubbleParameter(
1002
+ energyDensity(ctx.pot, ctx.phase_symmetric, percolation.Tperc), ctx.pot.conversionFactor
1003
+ )
1004
+ Rstar = RH / Hstar if np.isfinite(RH) and RH > 0 else np.nan
1005
+
1006
+ kappa_phi, kappa_sw, kappa_turb = calc_kappas(
1007
+ alpha_hydP,
1008
+ alpha_infP,
1009
+ alpha_eqP,
1010
+ ctx.derived_params.get("v_wall", 1.0),
1011
+ ctx.derived_params.get("c_s", 1 / np.sqrt(3)),
1012
+ Rstar,
1013
+ R0,
1014
+ ctx.GWconfig,
1015
+ )
1016
+ ctx.derived_params["kappa_phi"] = kappa_phi
1017
+ ctx.derived_params["kappa_sw"] = kappa_sw
1018
+ ctx.derived_params["kappa_turb"] = kappa_turb
1019
+
1020
+ def _finalize_results(self, ctx: TransitionContext) -> dict:
1021
+ """Ensure all requested parameters are present and return dict."""
1022
+ derived = ctx.derived_params
1023
+ beta_S3 = derived.get("betaH_S3", np.nan)
1024
+ beta_RH = derived.get("betaH_RH", np.nan)
1025
+
1026
+ if "WARNING:betaH_small" in ctx.derived_param_names:
1027
+ derived["WARNING:betaH_small"] = derived.get("betaH_RH", np.inf) < 10
1028
+
1029
+ if "WARNING:betaH_very_small" in ctx.derived_param_names:
1030
+ derived["WARNING:betaH_very_small"] = derived.get("betaH_RH", np.inf) < 3
1031
+
1032
+ if "WARNING:nucleationRate_nonexponential" in ctx.derived_param_names:
1033
+ derived["WARNING:nucleationRate_nonexponential"] = (
1034
+ np.isfinite(beta_S3) and beta_S3 < 0
1035
+ )
1036
+
1037
+ if "WARNING:betaH_nonfinite" in ctx.derived_param_names:
1038
+ derived["WARNING:betaH_nonfinite"] = (
1039
+ not np.isfinite(beta_S3) or not np.isfinite(beta_RH)
1040
+ )
1041
+
1042
+ if "WARNING:betaH_mismatch" in ctx.derived_param_names:
1043
+ mismatch = False
1044
+ if np.isfinite(beta_S3) and np.isfinite(beta_RH):
1045
+ abs_beta_S3 = abs(beta_S3)
1046
+ abs_beta_RH = abs(beta_RH)
1047
+ if abs_beta_S3 == 0 or abs_beta_RH == 0:
1048
+ mismatch = not np.isclose(abs_beta_S3, abs_beta_RH)
1049
+ else:
1050
+ mismatch = max(
1051
+ abs_beta_RH / abs_beta_S3,
1052
+ abs_beta_S3 / abs_beta_RH,
1053
+ ) >= 10
1054
+ derived["WARNING:betaH_mismatch"] = mismatch
1055
+
1056
+ if "WARNING:not_T0_global_min" in ctx.derived_param_names:
1057
+ # This flag is set in the check_T0_global_min function,
1058
+ # when checking all transitions after initialization.
1059
+ derived["WARNING:not_T0_global_min"] = False
1060
+
1061
+ # Check if there are any names in derived_param_names which are not
1062
+ # in the derived_params_dict
1063
+ for name in ctx.derived_param_names:
1064
+ if name not in derived:
1065
+ msg = (
1066
+ f"{name} not found in derived_params_dict. "
1067
+ "It might be that this variable still needs to be implemented. "
1068
+ "It is added to the dictionary with value NaN."
1069
+ )
1070
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
1071
+ derived[name] = np.nan
1072
+
1073
+ return derived
1074
+
1075
+ def computeTransitionObservables_one_transition(self, tr : TransitionInfo,
1076
+ rtol=1e-4, nAction=50):
1077
+ """Calculate the GW observables after the phase tracing and the
1078
+ computation of the nucleation temperature have been performed.
1079
+ This function assumes kinetic equilibrium between the dark sector and
1080
+ the SM.
1081
+
1082
+ Parameters
1083
+ ----------
1084
+ tr : TransitionInfo
1085
+ The transition information for which to compute observables.
1086
+ rtol : float, optional
1087
+ Relative tolerance for the computation. Default is 1e-4.
1088
+ nAction : int, optional
1089
+ Number of action points to consider. Default is 50.
1090
+ Returns
1091
+ ----------
1092
+ GWparams : tuple (float)
1093
+ Dictionary with the GW parameters."""
1094
+
1095
+ ctx = self._initialize_transition_context(tr)
1096
+ if ctx.verbose:
1097
+ if tr.Tnuc is None or not np.isfinite(tr.Tnuc):
1098
+ print(
1099
+ "\nStart calculating GW observables for transition from phase",
1100
+ tr.high_phase,
1101
+ "to phase",
1102
+ tr.low_phase,
1103
+ "without a nucleation-temperature seed",
1104
+ )
1105
+ else:
1106
+ print(
1107
+ "\nStart calculating GW observables for transition from phase",
1108
+ tr.high_phase,
1109
+ "to phase",
1110
+ tr.low_phase,
1111
+ "at Tnuc =",
1112
+ tr.Tnuc * self.pot.conversionFactor,
1113
+ "GeV",
1114
+ )
1115
+
1116
+ tmin = max(float(ctx.phase_symmetric.Tmin), float(ctx.phase_broken.Tmin))
1117
+ tmax = min(float(ctx.phase_symmetric.Tmax), float(ctx.phase_broken.Tmax))
1118
+ if tmin >= tmax:
1119
+ raise errors.PercolationError(
1120
+ "No overlapping temperature interval between traced phases: "
1121
+ f"Tsym in [{ctx.phase_symmetric.Tmin:.8g}, {ctx.phase_symmetric.Tmax:.8g}], "
1122
+ f"Tbro in [{ctx.phase_broken.Tmin:.8g}, {ctx.phase_broken.Tmax:.8g}]."
1123
+ )
1124
+
1125
+ percolation = self._compute_percolation(ctx, rtol, nAction)
1126
+ milestone_temperature = percolation.Tperc
1127
+ self._ensure_wall_velocity(ctx, milestone_temperature)
1128
+ self._ensure_sound_speed(ctx, milestone_temperature)
1129
+ alpha_theta_milestone = self._compute_transition_strength(ctx, milestone_temperature)
1130
+ ctx.derived_params["WARNING:too_weak_to_compute_perc"] = (
1131
+ np.isfinite(alpha_theta_milestone)
1132
+ and alpha_theta_milestone < ctx.PercolationConf.weak_threshold
1133
+ )
1134
+
1135
+ self._populate_temperature_observables(ctx, percolation)
1136
+ self._compute_alpha_parameters(ctx, percolation)
1137
+ self._compute_beta_and_separation(ctx, percolation, tmin, tmax)
1138
+ self._compute_kappa_parameters(ctx, percolation)
1139
+
1140
+ derived = ctx.derived_params
1141
+ if "D" in ctx.derived_param_names:
1142
+ derived["D"] = 1
1143
+ if "step" in ctx.derived_param_names:
1144
+ derived["step"] = ctx.tr.step
1145
+ if "total_steps" in ctx.derived_param_names:
1146
+ derived["total_steps"] = ctx.tr.total_steps
1147
+
1148
+ return self._finalize_results(ctx), percolation, ctx.outdict
1149
+
1150
+ def __iter__(self):
1151
+ """Enable iteration over the derived observables dictionary."""
1152
+ return iter(self.transitionObservables)
1153
+
1154
+ def __getitem__(self, key: str):
1155
+ """Allow dictionary-style access to an individual transition."""
1156
+ return self.transitionObservables[key]
1157
+
1158
+ def __len__(self):
1159
+ """Return the number of transitions for which observables were computed."""
1160
+ return len(self.transitionObservables)
1161
+
1162
+ def keys(self):
1163
+ """Expose the transition keys."""
1164
+ return self.transitionObservables.keys()
1165
+
1166
+ def items(self):
1167
+ """Return key/value pairs of the transition observables."""
1168
+ return self.transitionObservables.items()
1169
+
1170
+ def printDerivedParams(self, derived_param_names):
1171
+ """Print a rich-formatted table of the derived parameters per transition."""
1172
+ nTransitions = len(self.transitions)
1173
+ print("\nComputed thermodynamic quantities for", nTransitions, "transition(s):\n")
1174
+ for i, tr in enumerate(self.transitions):
1175
+ if tr.type == 1:
1176
+ derived_params_dict = self.transitionObservables[i]
1177
+ table = rich.table.Table(
1178
+ title=f"Transition {i+1}: First-order phase transition",
1179
+ title_justify="left",
1180
+ box=rich.box.ROUNDED,
1181
+ )
1182
+ table.add_column("Parameter", style="cyan", no_wrap=True)
1183
+ table.add_column("Value", style="orange1")
1184
+ for j, name in enumerate(derived_param_names):
1185
+ table.add_row(name, f"{derived_params_dict[name]:.10e}")
1186
+ console.print(table)
1187
+
1188
+ if tr.type == 2:
1189
+ table = rich.table.Table(
1190
+ title=f"Transition {i+1}: Second-order phase transition",
1191
+ title_justify="left",
1192
+ box=rich.box.ROUNDED,
1193
+ )
1194
+ table.add_column("Parameter", style="cyan", no_wrap=True)
1195
+ table.add_column("Value", style="orange1")
1196
+ table.add_row("Start phase", str(tr.high_phase))
1197
+ table.add_row("End phase", str(tr.low_phase))
1198
+ table.add_row("Tcrit / GeV", str(tr.Tcrit * self.pot.conversionFactor))
1199
+ console.print(table)