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,1781 @@
1
+ """Visualization utilities for TransitionListener potentials, spectra and scans.
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
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import numpy as np
15
+ from numpy.random import choice as _choice
16
+
17
+ import matplotlib.pyplot as plt
18
+ from matplotlib.ticker import (FixedLocator, FixedFormatter)
19
+
20
+ from transitionlistener import generic_potential, console
21
+ from .bubbledynamics import calcAction, Gamma, HubbleParameter, calcPercAndEvolve, energyDensity
22
+ from .transitions import Transitions
23
+ from .transitionObservables import (
24
+ TransitionObservables as _TransitionObservablesAdaptiveStepSize,
25
+ _extract_instanton_plot_arrays,
26
+ _select_instanton_near_temperature,
27
+ )
28
+ from .transitionObservables_fixedstep import TransitionObservables as _TransitionObservablesFixedStepSize
29
+
30
+
31
+ def _select_observables_class(pot):
32
+ """Pick the legacy seeded class when algorithm_mode == 'fixed_step_size'."""
33
+ if pot.config.percolationConf.algorithm_mode == "fixed_step_size":
34
+ return _TransitionObservablesFixedStepSize
35
+ return _TransitionObservablesAdaptiveStepSize
36
+ from .phases import Phases, reconstructTransitionHistory
37
+ from .plot_settings import plot_settings as TL_plot_settings
38
+ from .plot_settings import add_TL_logo
39
+ from .colors import *
40
+ import transitionlistener.constants as cn
41
+ from .observability import Observability
42
+ from .gwfopt import FOPTspectrum
43
+ from . import thermodynamics as td
44
+ from .helper_functions import import_file, load_potential
45
+
46
+ plt.rcParams.update(TL_plot_settings)
47
+
48
+ DATA_DIR = Path(__file__).resolve().parent / "tab_data"
49
+
50
+ class TLPlots():
51
+ """Collection of high-level plotting routines for a single potential instance."""
52
+
53
+ def __init__(self, inputparam_dict: dict, modelfile: str,
54
+ potential_name: str, output_folder_name: str = "",
55
+ plot_description: str = "",
56
+ potential=None, phases=None, transitions=None,
57
+ transition_observables=None, verbose: bool = False):
58
+ """Load the model and keep bookkeeping information for subsequent plots.
59
+
60
+ Parameters
61
+ ----------
62
+ inputparam_dict : dict
63
+ Dictionary of input parameters for the potential.
64
+ modelfile : str
65
+ Path to the model file defining the potential.
66
+ output_folder_name : str, optional
67
+ Folder where to save the plots.
68
+ plot_description : str, optional
69
+ Description to include in the plot titles.
70
+ """
71
+ self.inputparam_dict = inputparam_dict
72
+ self.modelfile = modelfile
73
+ self.output_folder_name = output_folder_name
74
+ self.plot_description = plot_description
75
+ self.verbose = verbose
76
+
77
+ if potential is not None:
78
+ self.pot = potential
79
+ else:
80
+ self.pot = load_potential(modelfile, potential_name)
81
+
82
+ if phases is not None:
83
+ self.phases = phases
84
+ if transitions is not None:
85
+ self.transitions = transitions
86
+ if transition_observables is not None:
87
+ self.derived_params_dict = transition_observables
88
+
89
+ # ------------------------------------------------------------------
90
+ # Internal helpers
91
+ # ------------------------------------------------------------------
92
+
93
+ def _ensure_phase_data(self, require_transitions: bool = False):
94
+ """Lazily populate phases, transitions, and observables needed by a plot."""
95
+ if not hasattr(self, "phases") or self.phases is None:
96
+ self.phases = Phases(self.pot, verbose=self.verbose)
97
+ if require_transitions:
98
+ if not hasattr(self, "transitions") or self.transitions is None:
99
+ self.transitions = Transitions(self.phases, self.pot, verbose=self.verbose)
100
+ if not hasattr(self, "derived_params_dict") or self.derived_params_dict is None:
101
+ self.derived_params_dict = _select_observables_class(self.pot)(
102
+ self.pot, self.phases, self.transitions, self.verbose
103
+ )
104
+
105
+ def _display_phase_label(self, phase_key) -> str:
106
+ """Return the short user-facing label for one phase."""
107
+ if hasattr(self, "phases") and self.phases is not None:
108
+ return self.phases.phase_alias(phase_key)
109
+ return str(phase_key)
110
+
111
+ def _resolve_phase_key(self, phase_key):
112
+ """Resolve a short user-facing phase label to the internal phase key."""
113
+ if hasattr(self, "phases") and self.phases is not None:
114
+ return self.phases.resolve_phase_key(phase_key)
115
+ return phase_key
116
+
117
+ @staticmethod
118
+ def _style_axes(ax):
119
+ """Apply the project-wide axis styling used by most TransitionListener plots."""
120
+ ax.tick_params(axis="both", which="both", labelsize=10, direction="in", width=0.5)
121
+ ax.xaxis.set_ticks_position("both")
122
+ ax.yaxis.set_ticks_position("both")
123
+ for spine in ["top", "bottom", "left", "right"]:
124
+ ax.spines[spine].set_linewidth(0.5)
125
+
126
+ @staticmethod
127
+ def _format_temperature_label(value: float, prefix: str = "T") -> str:
128
+ """Format a temperature label in GeV, switching to scientific notation when needed."""
129
+ if value < 1e-2 or value > 1e4:
130
+ exponent = int(np.floor(np.log10(value)))
131
+ mantissa = value / 10 ** exponent
132
+ return rf"{prefix} = {mantissa:.3f} \cdot 10^{{{exponent}}}\,\mathrm{{GeV}}"
133
+ return rf"{prefix} = {value:g}\,\mathrm{{GeV}}"
134
+
135
+ def _prepare_profile_axes(self, count: int, height: float = 3.0):
136
+ """Create a vertically stacked figure with consistent styling."""
137
+ fig, axes = plt.subplots(count, 1, figsize=(6.4, height * count))
138
+ if count == 1:
139
+ axes = [axes]
140
+ for ax in axes:
141
+ self._style_axes(ax)
142
+ return fig, axes
143
+
144
+ @staticmethod
145
+ def _normalize_profile_plot_arrays(radius, phi):
146
+ """Validate and normalize cached bounce-profile arrays for plotting."""
147
+ if radius is None or phi is None:
148
+ return None, None
149
+
150
+ radius_arr = np.asarray(radius, dtype=float).reshape(-1)
151
+ phi_arr = np.asarray(phi, dtype=float)
152
+ if phi_arr.ndim == 1:
153
+ phi_arr = phi_arr[:, np.newaxis]
154
+
155
+ if phi_arr.ndim != 2:
156
+ return None, None
157
+ if radius_arr.size == 0 or phi_arr.shape[0] == 0 or radius_arr.size != phi_arr.shape[0]:
158
+ return None, None
159
+ if not np.all(np.isfinite(radius_arr)) or not np.all(np.isfinite(phi_arr)):
160
+ return None, None
161
+ return radius_arr, phi_arr
162
+
163
+ def _transition_derived_params(self, transition_index: int, tr) -> dict:
164
+ """Return the observable dictionary associated with one transition."""
165
+ if hasattr(self, "derived_params_dict") and self.derived_params_dict is not None:
166
+ try:
167
+ derived = self.derived_params_dict[transition_index]
168
+ except Exception: # noqa: BLE001
169
+ derived = None
170
+ if isinstance(derived, dict):
171
+ return derived
172
+ if isinstance(getattr(tr, "derived_params", None), dict):
173
+ return tr.derived_params
174
+ return {}
175
+
176
+ # The adaptive/fixed-step observable containers cache extra per-transition
177
+ # data that plotting can reuse instead of relying on ``tr.instanton`` alone.
178
+ def _transition_percolation_result(self, transition_index: int):
179
+ """Return cached percolation metadata for one transition when available."""
180
+ percolation_results = getattr(getattr(self, "derived_params_dict", None), "percolationResults", None)
181
+ if isinstance(percolation_results, dict):
182
+ return percolation_results.get(transition_index)
183
+ return None
184
+
185
+ def _transition_outdict(self, transition_index: int):
186
+ """Return the cached action dictionary for one transition when available."""
187
+ outdicts = getattr(getattr(self, "derived_params_dict", None), "outdicts", None)
188
+ if isinstance(outdicts, dict):
189
+ return outdicts.get(transition_index)
190
+ return None
191
+
192
+ def _profile_temperature_candidates(self, transition_index: int, tr) -> list[float]:
193
+ """Return preferred temperatures at which to look up cached instantons."""
194
+ candidates: list[float] = []
195
+ if tr.Tnuc is not None and np.isfinite(tr.Tnuc):
196
+ candidates.append(float(tr.Tnuc))
197
+
198
+ derived = self._transition_derived_params(transition_index, tr)
199
+ tnuc_gev = derived.get("Tnuc_SM_GeV", np.nan)
200
+ if np.isfinite(tnuc_gev):
201
+ candidates.append(float(tnuc_gev) / self.pot.conversionFactor)
202
+
203
+ percolation = self._transition_percolation_result(transition_index)
204
+ tperc = getattr(percolation, "Tperc", np.nan)
205
+ if np.isfinite(tperc):
206
+ candidates.append(float(tperc))
207
+
208
+ unique: list[float] = []
209
+ for candidate in candidates:
210
+ if not any(np.isclose(candidate, seen) for seen in unique):
211
+ unique.append(candidate)
212
+ return unique
213
+
214
+ # Use the best available physical reference temperature for labeling and
215
+ # for choosing which cached instanton to recover.
216
+ def _profile_reference_temperature(self, transition_index: int, tr) -> tuple[float | None, str]:
217
+ """Return the best available temperature and label prefix for profile plots."""
218
+ if tr.Tnuc is not None and np.isfinite(tr.Tnuc):
219
+ return float(tr.Tnuc), r"T_\mathrm{nuc}"
220
+
221
+ derived = self._transition_derived_params(transition_index, tr)
222
+ tnuc_gev = derived.get("Tnuc_SM_GeV", np.nan)
223
+ if np.isfinite(tnuc_gev):
224
+ return float(tnuc_gev) / self.pot.conversionFactor, r"T_\mathrm{nuc}"
225
+
226
+ percolation = self._transition_percolation_result(transition_index)
227
+ tperc = getattr(percolation, "Tperc", np.nan)
228
+ if np.isfinite(tperc):
229
+ return float(tperc), r"T_\mathrm{perc}"
230
+
231
+ return None, "T"
232
+
233
+ def _profile_title(self, transition_index: int, tr) -> str:
234
+ """Build a title that reflects the temperature actually available."""
235
+ high_label = self._display_phase_label(tr.high_phase)
236
+ low_label = self._display_phase_label(tr.low_phase)
237
+ temperature, prefix = self._profile_reference_temperature(transition_index, tr)
238
+ if temperature is None:
239
+ return f"Bounce from {high_label} to {low_label}"
240
+
241
+ label = self._format_temperature_label(
242
+ float(temperature) * self.pot.conversionFactor,
243
+ prefix=prefix,
244
+ )
245
+ return fr"Bounce at ${label}$ from {high_label} to {low_label}"
246
+
247
+ def _phase_point_at_temperature(self, phase_key, fallback_point, temperature):
248
+ """Evaluate one phase minimum at ``temperature`` with a safe fallback."""
249
+ if (
250
+ temperature is not None
251
+ and np.isfinite(temperature)
252
+ and hasattr(self, "phases")
253
+ and self.phases is not None
254
+ ):
255
+ try:
256
+ phase = self.phases[phase_key]
257
+ point = np.asarray(phase.valAt(float(temperature)), dtype=float)
258
+ if np.all(np.isfinite(point)):
259
+ return point
260
+ except Exception: # noqa: BLE001
261
+ pass
262
+ return np.asarray(fallback_point, dtype=float)
263
+
264
+ @staticmethod
265
+ def _expanded_axis_limits(values, *, rel_pad: float, fallback_scale: float, min_pad: float):
266
+ """Return ``(low, high)`` with robust padding, even for zero-width data."""
267
+ arr = np.asarray(values, dtype=float).reshape(-1)
268
+ arr = arr[np.isfinite(arr)]
269
+ if arr.size == 0:
270
+ pad = max(float(min_pad), rel_pad * max(float(fallback_scale), 1.0))
271
+ return -pad, pad
272
+
273
+ low = float(np.min(arr))
274
+ high = float(np.max(arr))
275
+ span = high - low
276
+ if span > 0:
277
+ pad = max(rel_pad * span, float(min_pad))
278
+ else:
279
+ center = 0.5 * (low + high)
280
+ ref = max(abs(center), float(fallback_scale), float(min_pad))
281
+ pad = max(rel_pad * ref, float(min_pad))
282
+ return low - pad, high + pad
283
+
284
+ # Bounce profiles can come either from the transition object itself or from
285
+ # cached outdict entries keyed by temperature in the observables pipeline.
286
+ def _resolve_profile_plot_arrays(self, tr, transition_index: int):
287
+ """Resolve radius/field arrays for plotting, including seedless caches."""
288
+ candidates = [tr.instanton]
289
+ outdict = self._transition_outdict(transition_index)
290
+ for temperature in self._profile_temperature_candidates(transition_index, tr):
291
+ instanton = _select_instanton_near_temperature(outdict, temperature)
292
+ if instanton is not None:
293
+ candidates.append(instanton)
294
+
295
+ seen: set[int] = set()
296
+ for instanton in candidates:
297
+ if instanton is None:
298
+ continue
299
+ marker = id(instanton)
300
+ if marker in seen:
301
+ continue
302
+ seen.add(marker)
303
+ radius, phi = _extract_instanton_plot_arrays(instanton)
304
+ radius, phi = self._normalize_profile_plot_arrays(radius, phi)
305
+ if radius is not None and phi is not None:
306
+ return radius, phi
307
+
308
+ raise ValueError("No cached bubble profile is available for this transition.")
309
+
310
+ # Only first-order transitions with a recoverable cached profile should end
311
+ # up in ``profile`` / ``profileV``.
312
+ def _collect_profile_entries(self):
313
+ """Return FOPTs with resolved bounce profiles, skipping unavailable ones."""
314
+ entries = []
315
+ for transition_index, tr in enumerate(self.transitions):
316
+ if getattr(tr, "type", None) != 1:
317
+ continue
318
+ try:
319
+ radius, phi = self._resolve_profile_plot_arrays(tr, transition_index)
320
+ except ValueError as err:
321
+ high_label = self._display_phase_label(tr.high_phase)
322
+ low_label = self._display_phase_label(tr.low_phase)
323
+ console.print(
324
+ "[bold yellow]Skipping transition "
325
+ f"{high_label}->{low_label} in profile plots: {err}[/bold yellow]"
326
+ )
327
+ continue
328
+ entries.append((transition_index, tr, radius, phi))
329
+ return entries
330
+
331
+ def _plot_profile_axis(self, ax, tr, radius, phi, transition_index: int, ylabel: str):
332
+ """Render a single 1D bounce profile on the provided axes."""
333
+ for i in range(phi.shape[-1]):
334
+ lbl = r"$\phi_" + f"{i}" + r"$"
335
+ ax.semilogx(
336
+ radius / self.pot.conversionFactor,
337
+ phi[:, i] * self.pot.conversionFactor, label=lbl,
338
+ )
339
+ ax.set_ylabel(ylabel)
340
+ ax.legend()
341
+ ax.set_title(self._profile_title(transition_index, tr))
342
+
343
+ def plotAction(self, Tmin_GeV : float, Tmax_GeV : float, phase_indices : list, n : int = 100,
344
+ show : bool = False, save : bool = True, savetxt : bool = True):
345
+ """Visualise the Euclidean action and nucleation criterion across temperatures.
346
+
347
+ Parameters
348
+ ----------
349
+ Tmin_GeV : float
350
+ Minimum temperature in GeV.
351
+ Tmax_GeV : float
352
+ Maximum temperature in GeV.
353
+ phase_indices : list
354
+ List of phase indices to consider.
355
+ n : int, optional
356
+ Number of points to sample in the temperature range.
357
+ show : bool, optional
358
+ Whether to show the plot.
359
+ save : bool, optional
360
+ Whether to save the plot.
361
+
362
+ """
363
+ if not hasattr(self, "phases"):
364
+ self.phases = Phases(self.pot, verbose=self.verbose)
365
+ phase_indices = [self._resolve_phase_key(phase_indices[0]), self._resolve_phase_key(phase_indices[1])]
366
+
367
+ print("\nPlot action in the range of temperatures from %.2f GeV to %.2f GeV." % (Tmin_GeV, Tmax_GeV))
368
+ Trange = np.linspace(Tmin_GeV, Tmax_GeV, n) / self.pot.conversionFactor
369
+ S3 = np.zeros_like(Trange)
370
+ GH = np.zeros_like(Trange)
371
+
372
+ for i, T in enumerate(Trange):
373
+ print(f"Calculating action at T = {T * self.pot.conversionFactor:.2e} GeV.")
374
+ try:
375
+ S3_raw = calcAction(
376
+ self.pot,
377
+ T,
378
+ self.phases[phase_indices[1]],
379
+ self.phases[phase_indices[0]],
380
+ {},
381
+ verbose=True,
382
+ )
383
+ S3_val = float(np.squeeze(np.asarray(S3_raw, dtype=float)))
384
+ S3[i] = S3_val
385
+
386
+ G_raw = Gamma(T, S3_val)
387
+ H_raw = HubbleParameter(
388
+ energyDensity(self.pot, self.phases[phase_indices[1]], T),
389
+ self.pot.conversionFactor,
390
+ )
391
+ G = float(np.squeeze(np.asarray(G_raw, dtype=float)))
392
+ H = float(np.squeeze(np.asarray(H_raw, dtype=float)))
393
+ GH[i] = np.nan if H == 0 else G / H**4
394
+
395
+ print(
396
+ f"T / GeV: {T * self.pot.conversionFactor:.2e}, "
397
+ f"S3 / GeV: {float(S3_val * self.pot.conversionFactor):.2e}, "
398
+ f"Gamma / GeV^4: {float(G * self.pot.conversionFactor**4):.2e}, "
399
+ f"H / GeV: {float(H * self.pot.conversionFactor):.2e}, "
400
+ f"Gamma / H^4: {float(GH[i]):.2e}"
401
+ )
402
+ except Exception as e:
403
+ print(f"Error at T = {T * self.pot.conversionFactor:.2e} GeV: {e}")
404
+ S3[i] = np.nan
405
+ GH[i] = np.nan
406
+ continue
407
+ fig, axes = plt.subplots(2,1, sharex=True)
408
+ for ax in axes:
409
+ ax.tick_params(axis='both', which='both', labelsize=10, direction="in", width=0.5)
410
+ ax.xaxis.set_ticks_position('both')
411
+ ax.yaxis.set_ticks_position('both')
412
+ for axis in ['top','bottom','left','right']:
413
+ ax.spines[axis].set_linewidth(0.5)
414
+
415
+ ax = axes[0]
416
+ ax2 = axes[1]
417
+ ax.plot(Trange * self.pot.conversionFactor, S3 / Trange, label="Action")
418
+ ax2.plot(Trange * self.pot.conversionFactor, GH, label=r"$\Gamma / H^4$")
419
+ ax2.plot(Trange * self.pot.conversionFactor, np.ones_like(Trange), "k--")
420
+ ax2.set_xlabel(R"$T$ / $\mathrm{GeV}$")
421
+ ax.set_ylabel("Euclidean action $S_3/T$")
422
+ ax2.set_ylabel(r"$\Gamma / H^4$")
423
+ ax.set_title(self.plot_description)
424
+ ax.set_xlim(Tmin_GeV, Tmax_GeV)
425
+ ax.set_yscale("log")
426
+ ax2.set_yscale("log")
427
+ add_TL_logo(loc="lower left", magnification=2, ax=ax2)
428
+ fig.tight_layout()
429
+ if save: fig.savefig(self.output_folder_name + "action.pdf")
430
+ if show: plt.show()
431
+ if savetxt:
432
+ output_array = np.vstack((Trange * self.pot.conversionFactor,
433
+ S3 / Trange,
434
+ GH)).T
435
+ np.savetxt(self.output_folder_name + "action.txt", output_array,
436
+ header="T_GeV S3_over_T Gamma_over_H4", fmt="%.6e")
437
+
438
+ def plotPotential(self, plot_T_GeV : float, phi_ranges_GeV : list, n : int = 100,
439
+ show : bool = False, save : bool = True, log : bool = False):
440
+ """Plot the potential energy landscape at a fixed temperature.
441
+
442
+ Parameters
443
+ ----------
444
+ plot_T_GeV : float
445
+ Temperature in GeV at which to plot the potential.
446
+ phi_ranges_GeV : list
447
+ List defining the plotting ranges for each field dimension in GeV.
448
+ n : int, optional
449
+ Number of points to use for the plot.
450
+ show : bool, optional
451
+ Whether to show the plot.
452
+ save : bool, optional
453
+ Whether to save the plot.
454
+ log : bool, optional
455
+ Whether to use a logarithmic color scale (only for 2D plots).
456
+ """
457
+ if plot_T_GeV == 0.0:
458
+ T_str = r"$T = {:g}$".format(plot_T_GeV)
459
+ elif plot_T_GeV < 1e-2 or plot_T_GeV > 1e4:
460
+ exponent = int(np.floor(np.log10(plot_T_GeV)))
461
+ mantissa = plot_T_GeV / 10**exponent
462
+ T_str = r"$T = {:.3f} \cdot 10^{{{}}}$".format(mantissa, exponent)
463
+ else:
464
+ T_str = r"$T = {:g}$".format(plot_T_GeV)
465
+
466
+ if self.pot.Ndim ==1:
467
+ fig, ax = plot1d(
468
+ self.pot,
469
+ phi_ranges_GeV[0] / self.pot.conversionFactor,
470
+ phi_ranges_GeV[1] / self.pot.conversionFactor,
471
+ T=plot_T_GeV / self.pot.conversionFactor,
472
+ subtract=True,
473
+ n=n,
474
+ )
475
+ else:
476
+ fig, ax = plot2d(
477
+ self.pot,
478
+ np.array(
479
+ [phi_ranges_GeV[i] / self.pot.conversionFactor for i in range(len(phi_ranges_GeV))]
480
+ ),
481
+ T=plot_T_GeV / self.pot.conversionFactor,
482
+ treelevel=False,
483
+ offset=0,
484
+ xaxis=0,
485
+ yaxis=1,
486
+ n=n,
487
+ clevs=200,
488
+ cfrac=.8,
489
+ log=log,
490
+ )
491
+ ax.set_title(self.plot_description + f": Potential at {T_str}" + r"$\,\mathrm{GeV}$")
492
+ add_TL_logo(loc="upper left", ax=ax)
493
+ fig.tight_layout()
494
+ if save: fig.savefig(self.output_folder_name + "potential.pdf")
495
+ if show: plt.show()
496
+
497
+ def plotPercolation(
498
+ self,
499
+ show: bool = False,
500
+ Tmin_GeV: float = np.nan,
501
+ Tmax_GeV: float = np.nan,
502
+ n_action: int | None = None,
503
+ ):
504
+ """Plot the true-vacuum fraction to illustrate percolation."""
505
+ if not hasattr(self, "phases"):
506
+ self.phases = Phases(self.pot, verbose=self.verbose)
507
+ if not hasattr(self, "transitions"):
508
+ self.transitions = Transitions(self.phases, self.pot, self.verbose)
509
+ if not hasattr(self, "derived_params_dict"):
510
+ self.derived_params_dict = _select_observables_class(self.pot)(self.pot, self.phases, self.transitions, self.verbose)
511
+
512
+ nTransitions = len(self.transitions)
513
+ print("Found", nTransitions, "transitions.")
514
+
515
+ # Loop over transitions
516
+ for i, tr in enumerate(self.transitions):
517
+ print("Transition "+str(i+1)+":")
518
+ try:
519
+ if tr is not None:
520
+ if tr.type != 1:
521
+ print("Not a first-order transition, skipping.")
522
+ continue
523
+ Tnuc = tr.Tnuc
524
+ phase_symmetric = self.phases[tr.high_phase]
525
+ phase_broken = self.phases[tr.low_phase]
526
+ # Respect configured support points unless explicitly
527
+ # overridden for the percolation plot in the YAML file.
528
+ if n_action is None:
529
+ n_action_eff = int(getattr(self.pot.config.gwConf, "n_action", 25))
530
+ else:
531
+ n_action_eff = int(n_action)
532
+ if n_action_eff < 5:
533
+ n_action_eff = 5
534
+ if self.verbose:
535
+ plot_range_msg = (
536
+ f"[{Tmin_GeV}, {Tmax_GeV}] GeV"
537
+ if (np.isfinite(Tmin_GeV) or np.isfinite(Tmax_GeV))
538
+ else "automatic"
539
+ )
540
+ print(
541
+ "Running percolation diagnostics with "
542
+ f"n_action={n_action_eff}, "
543
+ f"T range={plot_range_msg}, "
544
+ f"Tnuc={Tnuc * self.pot.conversionFactor:.6g} GeV."
545
+ )
546
+ Tperc, TSYM, H, P, TBRO, S, Pr_exp, scalef_ratio, soundSpSq = calcPercAndEvolve(
547
+ {},
548
+ Tnuc,
549
+ phase_symmetric,
550
+ phase_broken,
551
+ self.pot,
552
+ vw=1,
553
+ nAction=n_action_eff,
554
+ verbose=self.verbose,
555
+ )
556
+
557
+ # Keep percolation diagnostics in a strip-like layout.
558
+ fig, ax = plt.subplots(1, 1, sharex=True, figsize=(6.4, 2.35))
559
+
560
+ ax.tick_params(axis='both', which='both', labelsize=10, direction="in", width=0.5)
561
+ ax.xaxis.set_ticks_position('both')
562
+ ax.yaxis.set_ticks_position('both')
563
+ for axis in ['top','bottom','left','right']:
564
+ ax.spines[axis].set_linewidth(0.5)
565
+
566
+ TSYM_GeV = np.asarray(TSYM, dtype=float) * self.pot.conversionFactor
567
+ P_arr = np.asarray(P, dtype=float)
568
+ mask = np.isfinite(TSYM_GeV) & np.isfinite(P_arr)
569
+ TSYM_GeV = TSYM_GeV[mask]
570
+ P_arr = P_arr[mask]
571
+
572
+ if TSYM_GeV.size == 0:
573
+ if self.verbose:
574
+ print("No finite percolation data points; skipping this transition.")
575
+ continue
576
+
577
+ # Keep the lower bound at the last temperature where
578
+ # percolation data exists.
579
+ Tmin_data_GeV = float(TSYM_GeV[-1])
580
+
581
+ Tnuc_GeV = Tnuc * self.pot.conversionFactor
582
+ Tperc_GeV = Tperc * self.pot.conversionFactor
583
+ if np.isfinite(Tmin_GeV):
584
+ Tmin_plot_GeV = float(Tmin_GeV)
585
+ if Tmin_plot_GeV > Tmin_data_GeV:
586
+ Tmin_plot_GeV = Tmin_data_GeV
587
+ else:
588
+ Tmin_plot_GeV = Tmin_data_GeV
589
+ Tmax_plot_GeV = Tnuc_GeV if not np.isfinite(Tmax_GeV) else float(Tmax_GeV)
590
+ if Tmax_plot_GeV <= Tmin_plot_GeV:
591
+ if self.verbose:
592
+ print(
593
+ "Invalid percolation plotting range "
594
+ f"[{Tmin_plot_GeV}, {Tmax_plot_GeV}] GeV; "
595
+ "falling back to automatic range."
596
+ )
597
+ Tmin_plot_GeV = Tmin_data_GeV
598
+ Tmax_plot_GeV = Tnuc_GeV
599
+
600
+ order = np.argsort(TSYM_GeV)
601
+ TSYM_GeV = TSYM_GeV[order]
602
+ P_arr = P_arr[order]
603
+
604
+ window = (TSYM_GeV >= Tmin_plot_GeV) & (TSYM_GeV <= Tmax_plot_GeV)
605
+ if np.count_nonzero(window) >= 2:
606
+ TSYM_plot = TSYM_GeV[window]
607
+ P_plot = P_arr[window]
608
+ else:
609
+ TSYM_plot = TSYM_GeV
610
+ P_plot = P_arr
611
+
612
+ if TSYM_plot.size >= 2:
613
+ n_dense = max(1200, 8 * TSYM_plot.size)
614
+ T_dense_GeV = np.linspace(TSYM_plot[0], TSYM_plot[-1], n_dense)
615
+ P_dense = np.interp(T_dense_GeV, TSYM_plot, P_plot)
616
+ else:
617
+ T_dense_GeV = TSYM_plot
618
+ P_dense = P_plot
619
+
620
+ ax.plot(T_dense_GeV, P_dense, label="$P_\\mathrm{t}(T)$")
621
+ ax.set_title(self.plot_description)
622
+ ax.set_ylabel("True vacuum fraction")
623
+ ax.axvline(Tperc_GeV, ls="--", color="black", label="$T_\\mathrm{perc}$")
624
+ ax.axvline(Tnuc_GeV, ls="-.", color="grey", label="$T_\\mathrm{nuc}$")
625
+ x_right_GeV = Tmax_plot_GeV
626
+ if not np.isfinite(Tmax_GeV):
627
+ xpad_GeV = max(0.02 * max(Tnuc_GeV - Tmin_plot_GeV, 0.0), 5.0e-5)
628
+ x_right_GeV = Tnuc_GeV + xpad_GeV
629
+ ax.set_xlim(Tmin_plot_GeV, x_right_GeV)
630
+ ax.set_xlabel(r"$T$ / $\mathrm{GeV}$")
631
+ ax.legend(loc="upper right")
632
+ add_TL_logo(loc="lower left", magnification=1.5, ax=ax)
633
+ fig.tight_layout()
634
+ if show:
635
+ plt.show()
636
+ else:
637
+ fig.savefig(self.output_folder_name + "percolation_" + str(i) + ".pdf")
638
+ except Exception as e:
639
+ print("Error in transition "+str(i)+":", e)
640
+ print("Skipping this transition.")
641
+ continue
642
+
643
+ def plotProfile(self):
644
+ """Plot bounce profiles along the tunnelling path for all FOPTs."""
645
+ self._ensure_phase_data(require_transitions=True)
646
+ has_fopts = any(getattr(tr, "type", None) == 1 for tr in self.transitions)
647
+ profile_entries = self._collect_profile_entries()
648
+ if not profile_entries:
649
+ message = (
650
+ "No cached bubble profiles available for profile plot."
651
+ if has_fopts
652
+ else "No first-order transitions available for profile plot."
653
+ )
654
+ console.print(f"[bold yellow]{message}[/bold yellow]")
655
+ return
656
+
657
+ fig, axes = self._prepare_profile_axes(len(profile_entries))
658
+ ylabel = r"$\phi_i$ / $\mathrm{GeV}$"
659
+ for ax, (transition_index, tr, radius, phi) in zip(axes, profile_entries):
660
+ self._plot_profile_axis(ax, tr, radius, phi, transition_index, ylabel)
661
+
662
+ axes[-1].set_xlabel(r"Bubble radius / $\mathrm{GeV}^{-1}$")
663
+ add_TL_logo(loc="lower right", magnification=2, ax=axes[-1])
664
+ fig.tight_layout()
665
+ fig.savefig(self.output_folder_name + "profile.pdf")
666
+
667
+ def plotProfileV(self, phi1 : int, phi2 : int):
668
+ """Show the potential along the tunnelling path between two field values.
669
+
670
+ Parameters
671
+ ----------
672
+ phi1 : int
673
+ Index of the first field dimension to plot.
674
+ phi2 : int
675
+ Index of the second field dimension to plot.
676
+
677
+ """
678
+ if self.pot.Ndim == 1:
679
+ raise ValueError("plotProfileV only works for Ndim > 1.")
680
+ if not hasattr(self, "phases"):
681
+ self.phases = Phases(self.pot, verbose=self.verbose)
682
+ if not hasattr(self, "transitions"):
683
+ self.transitions = Transitions(self.phases, self.pot, verbose=self.verbose)
684
+ if not hasattr(self, "derived_params_dict"):
685
+ self.derived_params_dict = _select_observables_class(self.pot)(self.pot, self.phases, self.transitions, self.verbose)
686
+
687
+ has_fopts = any(getattr(tr, "type", None) == 1 for tr in self.transitions)
688
+ profile_entries = self._collect_profile_entries()
689
+ if not profile_entries:
690
+ message = (
691
+ "No cached bubble profiles available for profileV plot."
692
+ if has_fopts
693
+ else "No first-order transitions available for profileV plot."
694
+ )
695
+ console.print(f"[bold yellow]{message}[/bold yellow]")
696
+ return
697
+
698
+ nFOPTs = len(profile_entries)
699
+
700
+ # the labels of the field axes we want to plot
701
+ xaxis=phi1
702
+ yaxis=phi2
703
+
704
+ fig, axes = plt.subplots(nFOPTs, 1, figsize=(6.4, 3 * nFOPTs))
705
+ if nFOPTs == 1:
706
+ axes = [axes]
707
+
708
+ for ax in axes:
709
+ ax.tick_params(axis='both', which='both', labelsize=10, direction="in", width=0.5)
710
+ ax.xaxis.set_ticks_position('both')
711
+ ax.yaxis.set_ticks_position('both')
712
+ for axis in ['top','bottom','left','right']:
713
+ ax.spines[axis].set_linewidth(0.5)
714
+
715
+ for idx, (transition_index, tr, _, phi) in enumerate(profile_entries):
716
+ mag = 0.2
717
+ plot_temperature, _ = self._profile_reference_temperature(transition_index, tr)
718
+ if plot_temperature is None:
719
+ plot_temperature = tr.Tcrit
720
+ start = self._phase_point_at_temperature(tr.high_phase, tr.high_vev, plot_temperature)
721
+ end = self._phase_point_at_temperature(tr.low_phase, tr.low_vev, plot_temperature)
722
+ fallback_scale = float(
723
+ max(
724
+ np.nanmax(np.abs(phi)),
725
+ np.nanmax(np.abs(start)),
726
+ np.nanmax(np.abs(end)),
727
+ 1.0 / max(self.pot.conversionFactor, 1.0),
728
+ )
729
+ )
730
+ min_pad = 0.05 / max(self.pot.conversionFactor, 1.0)
731
+ x0, x1 = self._expanded_axis_limits(
732
+ np.concatenate((phi[:, xaxis], [start[xaxis], end[xaxis]])),
733
+ rel_pad=mag,
734
+ fallback_scale=fallback_scale,
735
+ min_pad=min_pad,
736
+ )
737
+ y0, y1 = self._expanded_axis_limits(
738
+ np.concatenate((phi[:, yaxis], [start[yaxis], end[yaxis]])),
739
+ rel_pad=mag,
740
+ fallback_scale=fallback_scale,
741
+ min_pad=min_pad,
742
+ )
743
+ phi_ranges_GeV = np.array([x0, x1, y0, y1]) * self.pot.conversionFactor
744
+ plot2d(
745
+ self.pot,
746
+ np.array([x0, x1, y0, y1]),
747
+ axes[idx],
748
+ T=plot_temperature,
749
+ treelevel=False,
750
+ offset=0,
751
+ xaxis=xaxis,
752
+ yaxis=yaxis,
753
+ n=100,
754
+ clevs=200,
755
+ cfrac=.8,
756
+ )
757
+ axes[idx].plot(
758
+ start[xaxis] * self.pot.conversionFactor,
759
+ start[yaxis] * self.pot.conversionFactor,
760
+ label="Start",
761
+ marker="^",
762
+ lw=0,
763
+ markersize=8,
764
+ color="C3",
765
+ )
766
+ axes[idx].plot(
767
+ end[xaxis] * self.pot.conversionFactor,
768
+ end[yaxis] * self.pot.conversionFactor,
769
+ label="End",
770
+ marker="x",
771
+ lw=0,
772
+ markersize=8,
773
+ color="C5",
774
+ )
775
+ axes[idx].plot(
776
+ phi[:, xaxis] * self.pot.conversionFactor,
777
+ phi[:, yaxis] * self.pot.conversionFactor,
778
+ color="C2",
779
+ lw=3,
780
+ alpha=1,
781
+ )
782
+ axes[idx].legend(loc="upper right")
783
+ axes[idx].set_title(self._profile_title(transition_index, tr))
784
+ axes[idx].set_xlim(phi_ranges_GeV[0], phi_ranges_GeV[1])
785
+ axes[idx].set_ylim(phi_ranges_GeV[2], phi_ranges_GeV[3])
786
+ add_TL_logo(loc="lower right", magnification=1, ax=axes[-1])
787
+ fig.tight_layout()
788
+ fig.savefig(self.output_folder_name + "profileV.pdf")
789
+
790
+ def plotPhases(self, Tmin_GeV: float = np.nan, Tmax_GeV: float = np.nan,
791
+ include_transitions: bool = True, save: bool = True,
792
+ show: bool = False, plot_squaresum: bool = True):
793
+ """Plot the traced phase trajectories and optionally their transitions.
794
+
795
+ ``plot_squaresum`` only matters in multi-field models; when ``False``
796
+ the panel for ``sqrt(sum_i phi_i^2)`` is omitted, giving a 2-panel
797
+ figure that matches the 1-field default layout.
798
+ """
799
+ if include_transitions:
800
+ # First produce the plot immediately after phase tracing.
801
+ self._ensure_phase_data(require_transitions=False)
802
+ self._render_phase_plot(
803
+ include_transitions=False,
804
+ Tmin_GeV=Tmin_GeV,
805
+ Tmax_GeV=Tmax_GeV,
806
+ save_path=self.output_folder_name + "phases.pdf" if save else None,
807
+ show=show,
808
+ plot_squaresum=plot_squaresum,
809
+ )
810
+ # Then ensure transitions exist and overlay the full history.
811
+ self._ensure_phase_data(require_transitions=True)
812
+ self._render_phase_plot(
813
+ include_transitions=True,
814
+ Tmin_GeV=Tmin_GeV,
815
+ Tmax_GeV=Tmax_GeV,
816
+ save_path=self.output_folder_name + "phases_with_transitions.pdf" if save else None,
817
+ show=show,
818
+ plot_squaresum=plot_squaresum,
819
+ )
820
+ else:
821
+ self._ensure_phase_data(require_transitions=False)
822
+ self._render_phase_plot(
823
+ include_transitions=False,
824
+ Tmin_GeV=Tmin_GeV,
825
+ Tmax_GeV=Tmax_GeV,
826
+ save_path=self.output_folder_name + "phases.pdf" if save else None,
827
+ show=show,
828
+ plot_squaresum=plot_squaresum,
829
+ )
830
+
831
+ def _render_phase_plot(self, include_transitions: bool, Tmin_GeV: float, Tmax_GeV: float,
832
+ save_path: Optional[str], show: bool,
833
+ plot_squaresum: bool = True) -> None:
834
+ """Render a single phase plot with or without transition overlays."""
835
+ CF = self.pot.conversionFactor
836
+ color = ["C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9"]
837
+ ls = ["-", "--", "-.", ":", "-", "--", "-.", ":", "-", "--", "-."]
838
+
839
+ if self.pot.Ndim == 1 or not plot_squaresum:
840
+ fig, axes = plt.subplots(2, 1, sharex=True, figsize=(6.4, 5), edgecolor="white")
841
+ else:
842
+ fig, axes = plt.subplots(3, 1, sharex=True, figsize=(6.4, 7), edgecolor="white")
843
+
844
+ for ax in axes:
845
+ self._style_axes(ax)
846
+
847
+ for i, key in enumerate(self.phases.keys()):
848
+ p = self.phases[key]
849
+ DeltaV = self.pot.Vtot(p.X, p.T) - self.pot.Vtot(p.X * 0, p.T)
850
+ for j in range(self.pot.Ndim):
851
+ axes[0].plot(p.T * CF, p.X[:, j] * CF, color=color[i % len(color)], ls=ls[j],
852
+ label=fr"{self._display_phase_label(key)}, $\phi_{j+1}$")
853
+ if self.pot.Ndim > 1 and plot_squaresum:
854
+ squaresum = np.sum(p.X ** 2, axis=1)
855
+ axes[1].plot(p.T * CF, np.sqrt(squaresum) * CF, color=color[i % len(color)], ls=ls[0])
856
+ axes[-1].plot(p.T * CF, DeltaV * CF ** 4, color=color[i % len(color)], ls=ls[0])
857
+
858
+ if include_transitions:
859
+ if not hasattr(self, "phi_min"):
860
+ self.phi_min = reconstructTransitionHistory(self.phases, self.transitions)
861
+
862
+ lower_bound = None if np.isnan(Tmin_GeV) else Tmin_GeV / CF
863
+ upper_bound = None if np.isnan(Tmax_GeV) else Tmax_GeV / CF
864
+ phi_label_added = False
865
+ for idx, segment in self.phi_min.items():
866
+ temps = segment.temperature_grid(
867
+ lower=lower_bound,
868
+ upper=upper_bound,
869
+ min_points=max(50, segment.phase.T.size),
870
+ )
871
+ if temps.size < 2:
872
+ continue
873
+ phi_vals = np.asarray(segment(temps))
874
+ if phi_vals.ndim == 1:
875
+ phi_vals = phi_vals.reshape(-1, 1)
876
+ label = r"$\phi_{\min}^\mathrm{glob}(T)$" if not phi_label_added else None
877
+ for j in range(self.pot.Ndim):
878
+ axes[0].plot(
879
+ temps * CF,
880
+ phi_vals[:, j] * CF,
881
+ color="black",
882
+ lw=1.3,
883
+ label=label if j == 0 else None,
884
+ zorder=5,
885
+ ls="dotted",
886
+ )
887
+ if self.pot.Ndim > 1 and plot_squaresum:
888
+ magnitude = np.linalg.norm(phi_vals, axis=1)
889
+ axes[1].plot(
890
+ temps * CF,
891
+ magnitude * CF,
892
+ color="black",
893
+ lw=1.3,
894
+ zorder=5,
895
+ ls="dotted",
896
+ )
897
+ zero_field = np.zeros_like(phi_vals)
898
+ delta_v = self.pot.Vtot(phi_vals, temps) - self.pot.Vtot(zero_field, temps)
899
+ axes[-1].plot(
900
+ temps * CF,
901
+ delta_v * CF**4,
902
+ color="black",
903
+ lw=1.3,
904
+ zorder=5,
905
+ ls="dotted",
906
+ )
907
+ phi_label_added = True
908
+
909
+ for tr in self.transitions:
910
+ if getattr(tr, "type", 0) == 1:
911
+ Tperc_GeV = tr.derived_params.get("Tperc_SM_GeV", np.nan)
912
+ Treh_GeV = tr.derived_params.get("Treh_SM_GeV", np.nan)
913
+ for ax in axes:
914
+ ax.axvline(Tperc_GeV, ls="--", color=DESYpetrol, alpha=0.5)
915
+ ax.axvline(Treh_GeV, ls="-.", color=DESYmagenta, alpha=0.5)
916
+ if getattr(tr, "type", 0) == 2:
917
+ Tcrit_GeV = tr.Tcrit * CF
918
+ for ax in axes:
919
+ ax.axvline(Tcrit_GeV, ls="-", color=DESYgelb, alpha=0.5)
920
+
921
+ axes[-1].legend(loc="upper right", handles=[
922
+ plt.Line2D([0], [0], color=DESYpetrol, linestyle="--", label=r"$T_\mathrm{perc}$", alpha=0.5),
923
+ plt.Line2D([0], [0], color=DESYmagenta, linestyle="-.", label=r"$T_\mathrm{reh}$", alpha=0.5),
924
+ plt.Line2D([0], [0], color=DESYgelb, linestyle="-", label=r"$T_\mathrm{crit}$", alpha=0.5),
925
+ ])
926
+
927
+ add_TL_logo(loc="lower right", magnification=2, ax=axes[-1])
928
+ axes[0].set_title(self.plot_description)
929
+ axes[0].legend(loc="upper right")
930
+ axes[0].set_ylabel(r"$\phi_i$ / $\mathrm{GeV}$")
931
+ if self.pot.Ndim > 1 and plot_squaresum:
932
+ axes[1].set_ylabel(r"$\sqrt{\sum_i \phi_i^2}$ / $\mathrm{GeV}$")
933
+ axes[-1].set_ylabel(r"$[V(\phi_{min}(T), T) - V(0, T)] / \mathrm{GeV^4}$")
934
+
935
+ if not np.isnan(Tmin_GeV) and not np.isnan(Tmax_GeV):
936
+ axes[-1].set_xlim(Tmin_GeV, Tmax_GeV)
937
+ else:
938
+ axes[-1].set_xlim(self.pot.Tmin * CF, self.pot.Tmax * CF)
939
+ axes[-1].set_xlabel(r"$T$ / $\mathrm{GeV}$")
940
+ fig.tight_layout()
941
+
942
+ if save_path:
943
+ fig.savefig(save_path)
944
+ if show:
945
+ plt.show()
946
+ plt.close(fig)
947
+
948
+ def plotDOFs(self, Tmin_GeV : float =np.nan, Tmax_GeV : float =np.nan):
949
+ """Plot the effective degrees of freedom in the SM and dark sector.
950
+
951
+ Parameters
952
+ ----------
953
+ Tmin_GeV : float, optional
954
+ Minimum temperature in GeV for the x-axis.
955
+ Tmax_GeV : float, optional
956
+ Maximum temperature in GeV for the x-axis.
957
+ """
958
+ if not hasattr(self, "phases"):
959
+ self.phases = Phases(self.pot, verbose=self.verbose)
960
+ if not hasattr(self, "transitions"):
961
+ self.transitions = Transitions(self.phases, self.pot, verbose=self.verbose)
962
+ if not hasattr(self, "derived_params_dict"):
963
+ self.derived_params_dict = _select_observables_class(self.pot)(self.pot, self.phases, self.transitions, self.verbose)
964
+ if not hasattr(self, "phi_min"):
965
+ self.phi_min = reconstructTransitionHistory(self.phases, self.transitions)
966
+
967
+ CF = self.pot.conversionFactor
968
+ history = self.phi_min
969
+ history_Tmin, history_Tmax = history.temperature_span
970
+
971
+ Tmin = history_Tmin if np.isnan(Tmin_GeV) else Tmin_GeV / CF
972
+ Tmax = history_Tmax if np.isnan(Tmax_GeV) else Tmax_GeV / CF
973
+
974
+ if Tmax <= Tmin:
975
+ raise ValueError("Requested temperature range for plotDOFs has zero or negative span.")
976
+
977
+ fig, axes = plt.subplots(2,1, sharex=True, figsize=(6.4, 6), edgecolor="white")
978
+ for a in axes:
979
+ a.tick_params(axis='both', which='both', labelsize=10, direction="in", width=0.5)
980
+ a.xaxis.set_ticks_position('both')
981
+ a.yaxis.set_ticks_position('both')
982
+ for axis in ['top','bottom','left','right']:
983
+ a.spines[axis].set_linewidth(0.5)
984
+
985
+ label_usage_geff = {"BSM": False, "SM": False, "Total": False}
986
+ label_usage_rho = {"BSM": False, "SM": False, "Total": False}
987
+ for idx, segment in history.items():
988
+ seg_low, seg_high = segment.bounds()
989
+ seg_low = max(seg_low, Tmin)
990
+ seg_high = min(seg_high, Tmax)
991
+ if seg_high <= seg_low:
992
+ continue
993
+
994
+ # Use a denser sampling for smoother g_eff and rho(T) curves.
995
+ min_points = int(np.clip(max(400, 2 * segment.phase.T.size), 400, 2000))
996
+ temps = segment.temperature_grid(lower=Tmin, upper=Tmax, min_points=min_points)
997
+ if temps.size < min_points:
998
+ temps = np.linspace(seg_low, seg_high, min_points)
999
+
1000
+ phi_vals = np.asarray(segment(temps))
1001
+ if phi_vals.ndim == 1:
1002
+ phi_vals = phi_vals.reshape(-1, 1)
1003
+
1004
+ geffDS = []
1005
+ geffSM = []
1006
+ for phi_vec, T in zip(phi_vals, temps):
1007
+ # Use the mass-spectrum tuple API directly; this is supported
1008
+ # by all model files and avoids relying on optional sector wrappers.
1009
+ bosons = self.pot.boson_massSq(phi_vec, 0)
1010
+ fermions = self.pot.fermion_massSq(phi_vec)
1011
+ geffDS.append(td.e_geffDS(bosons, fermions, T))
1012
+ geffSM.append(td.e_geffSM(T, CF))
1013
+
1014
+ geffDS = np.asarray(geffDS)
1015
+ geffSM = np.asarray(geffSM)
1016
+ rhoDS = np.pi**2 / 30 * geffDS * temps**4
1017
+ rhoSM = np.pi**2 / 30 * geffSM * temps**4
1018
+
1019
+ temps_plot = temps * CF
1020
+
1021
+ label = None if label_usage_geff["BSM"] else "BSM"
1022
+ axes[0].plot(temps_plot, geffDS, color="C0", label=label)
1023
+ if label:
1024
+ label_usage_geff["BSM"] = True
1025
+
1026
+ label = None if label_usage_geff["SM"] else "SM"
1027
+ axes[0].plot(temps_plot, geffSM, color="C1", label=label)
1028
+ if label:
1029
+ label_usage_geff["SM"] = True
1030
+
1031
+ label = None if label_usage_geff["Total"] else "Total"
1032
+ axes[0].plot(temps_plot, geffDS + geffSM, color="black", label=label)
1033
+ if label:
1034
+ label_usage_geff["Total"] = True
1035
+
1036
+ label = None if label_usage_rho["BSM"] else "BSM"
1037
+ axes[1].plot(temps_plot, rhoDS * CF**4, color="C0", label=label)
1038
+ if label:
1039
+ label_usage_rho["BSM"] = True
1040
+
1041
+ label = None if label_usage_rho["SM"] else "SM"
1042
+ axes[1].plot(temps_plot, rhoSM * CF**4, color="C1", label=label)
1043
+ if label:
1044
+ label_usage_rho["SM"] = True
1045
+
1046
+ label = None if label_usage_rho["Total"] else "Total"
1047
+ axes[1].plot(temps_plot, (rhoDS + rhoSM) * CF**4, color="black", label=label)
1048
+ if label:
1049
+ label_usage_rho["Total"] = True
1050
+
1051
+ no_label1 = False # these two make sure we only label the lines once
1052
+ no_label2 = False
1053
+ for tr in self.transitions:
1054
+ if tr.type == 1:
1055
+ Tperc_GeV = tr.derived_params["Tperc_SM_GeV"]
1056
+ Treh_GeV = tr.derived_params["Treh_SM_GeV"]
1057
+ if not no_label1:
1058
+ for ax in axes:
1059
+ ax.axvline(Tperc_GeV, ls="--", color=DESYpetrol, alpha=0.5, label=r"$T_\mathrm{perc}$")
1060
+ ax.axvline(Treh_GeV, ls="-.", color=DESYmagenta, alpha=0.5, label=r"$T_\mathrm{reh}$")
1061
+ else:
1062
+ for ax in axes:
1063
+ ax.axvline(Tperc_GeV, ls="--", color=DESYpetrol, alpha=0.5)
1064
+ ax.axvline(Treh_GeV, ls="-.", color=DESYmagenta, alpha=0.5)
1065
+ no_label1 = True
1066
+ if tr.type == 2:
1067
+ Tcrit_GeV = tr.Tcrit * self.pot.conversionFactor
1068
+ if not no_label2:
1069
+ for ax in axes:
1070
+ ax.axvline(Tcrit_GeV, ls="-", color=DESYgelb, alpha=0.5, label=r"$T_\mathrm{crit}$")
1071
+ else:
1072
+ for ax in axes:
1073
+ ax.axvline(Tcrit_GeV, ls="-", color=DESYgelb, alpha=0.5)
1074
+ no_label2 = True
1075
+
1076
+ add_TL_logo(loc="lower right", ax=axes[1], magnification=2)
1077
+ axes[0].set_title(self.plot_description)
1078
+ axes[0].legend(loc="upper right")
1079
+ axes[0].set_ylabel(R"$g_{\mathrm{eff}}(T)$")
1080
+ axes[1].set_ylabel(R"$\rho(T) / \mathrm{GeV}^4$")
1081
+ axes[1].set_yscale("log")
1082
+ axes[0].set_xlim(Tmin * CF, Tmax * CF)
1083
+ axes[1].set_xlim(Tmin * CF, Tmax * CF)
1084
+ axes[1].set_xlabel(R"$T$ / $\mathrm{GeV}$")
1085
+ fig.tight_layout()
1086
+ fig.savefig(self.output_folder_name + "dofs.pdf")
1087
+
1088
+ def plotEnergyDensity(self, Tmin_GeV : float =np.nan, Tmax_GeV : float =np.nan,
1089
+ show_plot=False):
1090
+ """Plot the energy density evolution for each phase.
1091
+
1092
+ Parameters
1093
+ ----------
1094
+ Tmin_GeV : float, optional
1095
+ Minimum temperature in GeV for the x-axis.
1096
+ Tmax_GeV : float, optional
1097
+ Maximum temperature in GeV for the x-axis.
1098
+ """
1099
+ if not hasattr(self, "phases"):
1100
+ self.phases = Phases(self.pot, verbose=self.verbose)
1101
+
1102
+ fig, ax = plt.subplots(1,1, sharex=True, figsize=(6.4, 4.8), edgecolor="white")
1103
+ for a in (ax,):
1104
+ a.tick_params(axis='both', which='both', labelsize=10, direction="in", width=0.5)
1105
+ a.xaxis.set_ticks_position('both')
1106
+ a.yaxis.set_ticks_position('both')
1107
+ for axis in ['top','bottom','left','right']:
1108
+ a.spines[axis].set_linewidth(0.5)
1109
+
1110
+ Tmin = self.pot.Tmin if np.isnan(Tmin_GeV) else Tmin_GeV / self.pot.conversionFactor
1111
+ Tmax = self.pot.Tmax if np.isnan(Tmax_GeV) else Tmax_GeV / self.pot.conversionFactor
1112
+ Trange = np.exp(np.linspace(np.log(Tmin), np.log(Tmax), 1000))
1113
+
1114
+ for i, phase_key in enumerate(self.phases.keys()):
1115
+ Trange_phase = Trange[(Trange >= self.phases[phase_key].Tmin) & (Trange <= self.phases[phase_key].Tmax)]
1116
+ if len(Trange_phase) == 0:
1117
+ continue
1118
+ rho = np.array([energyDensity(self.pot, self.phases[phase_key], T) for T in Trange_phase])
1119
+ ax.plot(
1120
+ Trange_phase * self.pot.conversionFactor,
1121
+ rho * self.pot.conversionFactor**4,
1122
+ label=self._display_phase_label(phase_key),
1123
+ )
1124
+
1125
+ add_TL_logo(loc="lower right", ax=ax)
1126
+ ax.set_title(self.plot_description)
1127
+ ax.legend(loc="upper right")
1128
+ ax.set_ylabel(R"$\rho$ / $\mathrm{GeV}^4$")
1129
+ ax.set_yscale("log")
1130
+ ax.set_xscale("log")
1131
+ #ax.set_ylim(1e-2, 1e11)
1132
+ ax.set_xlim(Trange[0] * self.pot.conversionFactor, Trange[-1] * self.pot.conversionFactor)
1133
+ ax.set_xlabel(R"$T$ / $\mathrm{GeV}$")
1134
+ fig.tight_layout()
1135
+ if show_plot:
1136
+ plt.show()
1137
+ else:
1138
+ fig.savefig(self.output_folder_name + "rho.pdf")
1139
+
1140
+ def plotSensitivities(gw, showplot : bool =False, call_from_spectrum=False, nfreq : int =5,
1141
+ fig=None, ax=None):
1142
+ """Plot ground- and space-based detector sensitivities alongside the spectrum.
1143
+
1144
+ Parameters
1145
+ ----------
1146
+ gw : GWSpectrum
1147
+ The GWSpectrum object containing the sensitivity data.
1148
+ showplot : bool, optional
1149
+ Whether to display the plot.
1150
+ call_from_spectrum : bool, optional
1151
+ Whether the function is called from within a spectrum plot.
1152
+ If True, the function returns the figure and axis objects instead
1153
+ of showing or saving the plot.
1154
+ nfreq : int, optional
1155
+ Number of frequency bins for the PTA violins.
1156
+ """
1157
+ if ax == None:
1158
+ fig, ax = plt.subplots(figsize=(6.4, 3.5))
1159
+ transparency = 1
1160
+
1161
+ for det_name in gw.det:
1162
+ if gw.det[det_name]["no_plot"]:
1163
+ continue
1164
+
1165
+ is_existing = det_name in {"NANOGrav_15_yrs", "HLV_O2"}
1166
+
1167
+ ax.fill_between(
1168
+ np.log10(gw.det[det_name]["f_PLI"]),
1169
+ np.log10(gw.det[det_name]["PLI"]),
1170
+ 0,
1171
+ alpha=1,
1172
+ color="white",
1173
+ zorder=gw.det[det_name]["zorder"],
1174
+ lw=0,
1175
+ )
1176
+
1177
+ ax.fill_between(
1178
+ np.log10(gw.det[det_name]["f_PLI"]),
1179
+ np.log10(gw.det[det_name]["PLI"]),
1180
+ 0,
1181
+ alpha=0.8,
1182
+ color=gw.det[det_name]["color"],
1183
+ zorder=gw.det[det_name]["zorder"],
1184
+ lw=0,
1185
+ )
1186
+ if is_existing:
1187
+ ax.fill_between(
1188
+ np.log10(gw.det[det_name]["f_PLI"]),
1189
+ np.log10(gw.det[det_name]["PLI"]),
1190
+ 0,
1191
+ alpha=transparency,
1192
+ color=gw.det[det_name]["color"],
1193
+ zorder=gw.det[det_name]["zorder"],
1194
+ label=gw.det[det_name]["plot_name"],
1195
+ lw=0,
1196
+ )
1197
+ else:
1198
+ # Predicted sensitivity: draw as hatched region, no solid fill
1199
+ ax.fill_between(
1200
+ np.log10(gw.det[det_name]["f_PLI"]),
1201
+ np.log10(gw.det[det_name]["PLI"]),
1202
+ 0,
1203
+ alpha=0,
1204
+ color="none",
1205
+ hatch="\\\\",
1206
+ edgecolor=gw.det[det_name]["color"],
1207
+ linewidth=0.0,
1208
+ zorder=gw.det[det_name]["zorder"],
1209
+ label=gw.det[det_name]["plot_name"],
1210
+ )
1211
+
1212
+
1213
+
1214
+ add_violins(ax, violincolor="#C0105A", nfreq=nfreq)
1215
+ add_NeffHatch(ax, Neffcolor="grey", h2OmegaNeff=2.48e-7)
1216
+ if not call_from_spectrum:
1217
+ ax.set_title("PLI sensitivities and PTA violins")
1218
+ ax.set_xlabel(R"$f$ / $\mathrm{Hz}$")
1219
+ ax.set_ylabel(R'$h^2 \Omega_\mathrm{GW} (f)$')
1220
+
1221
+ ax.tick_params(axis='both', which='both', direction="in", width=0.5, zorder=1000)
1222
+ ax.xaxis.set_ticks_position('both')
1223
+ ax.yaxis.set_ticks_position('both')
1224
+ for axis in ["top", 'bottom','left','right']:
1225
+ ax.spines[axis].set_linewidth(0.5)
1226
+ ax.spines[axis].set_zorder(10000)
1227
+
1228
+ xlim = [-10,4]
1229
+ xa = np.linspace(int(xlim[0]), int(xlim[1]), int(xlim[1] - xlim[0] + 1))
1230
+ xtMajor = [j for j in xa]
1231
+ xtMinor = [np.log10(i*10**j) for j in xa for i in range(10)[1:10]]
1232
+ label_map = {-9: "nHz", -6: "µHz", -3: "mHz", 0: "Hz", 3: "kHz"}
1233
+ xlMajor = [label_map.get(i, "") for i in xa]
1234
+ xMajorLocator = FixedLocator(xtMajor)
1235
+ xMinorLocator = FixedLocator(xtMinor)
1236
+ xMajorFormatter = FixedFormatter(xlMajor)
1237
+ ax.xaxis.set_major_locator(xMajorLocator)
1238
+ ax.xaxis.set_minor_locator(xMinorLocator)
1239
+ ax.xaxis.set_major_formatter(xMajorFormatter)
1240
+ ax.set_xlim(-9.5, 3.5)
1241
+
1242
+ ylim = [-18, -6]
1243
+ ya = np.linspace(ylim[0], ylim[1], int(ylim[1] - ylim[0] + 1))
1244
+ ytMajor = [j for j in ya]
1245
+ ytMinor = [np.log10(i*10**j) for i in range(10)[1:10] for j in ya]
1246
+ ylMajor = [r"$10^{" + str(int(i)) + "}$" if i%2==1 else "" for i in ya]
1247
+ yMajorLocator = FixedLocator(ytMajor)
1248
+ yMinorLocator = FixedLocator(ytMinor)
1249
+ yMajorFormatter = FixedFormatter(ylMajor)
1250
+ ax.yaxis.set_major_locator(yMajorLocator)
1251
+ ax.yaxis.set_minor_locator(yMinorLocator)
1252
+ ax.yaxis.set_major_formatter(yMajorFormatter)
1253
+ ax.set_ylim(ylim[0], ylim[1])
1254
+ ax.yaxis.set_zorder(1000)
1255
+ ax.xaxis.set_zorder(1000)
1256
+ ax.set_ylim(-17.5, -6)
1257
+
1258
+ #ax.grid()
1259
+ add_TL_logo(loc="outside", ax=ax, small_logo=False,
1260
+ magnification=1.4)
1261
+ ax.legend(bbox_to_anchor=(1, 0.7), ncol=1)
1262
+ if not call_from_spectrum:
1263
+ plt.tight_layout()
1264
+ if showplot:
1265
+ plt.show()
1266
+ else:
1267
+ from pathlib import Path
1268
+ Path(gw.foldername).mkdir(parents=True, exist_ok=True)
1269
+ plt.savefig(gw.foldername+"GW_sensitivities.pdf")
1270
+ plt.close(fig)
1271
+ else:
1272
+ return fig, ax
1273
+
1274
+ def plotSensitivitiesPTA(gw, showplot : bool = False, call_from_spectrum : bool = False, nfreq : int = 5):
1275
+ """Plot PTA sensitivity curves alongside the spectrum.
1276
+
1277
+ Parameters
1278
+ ----------
1279
+ gw : GWSpectrum
1280
+ The GWSpectrum object containing the sensitivity data.
1281
+ showplot : bool, optional
1282
+ Whether to display the plot.
1283
+ call_from_spectrum : bool, optional
1284
+ Whether the function is called from within a spectrum plot.
1285
+ If True, the function returns the figure and axis objects instead
1286
+ of showing or saving the plot.
1287
+ nfreq : int, optional
1288
+ Number of frequency bins for the PTA violins.
1289
+ """
1290
+ fig, ax = plt.subplots()
1291
+ transparency = 0.5
1292
+
1293
+ for det_name in gw.det:
1294
+ if gw.det[det_name]["no_plot"]:
1295
+ continue
1296
+
1297
+ is_existing = det_name in {"NANOGrav_15_yrs", "HLV_O2"}
1298
+
1299
+ ax.fill_between(
1300
+ np.log10(gw.det[det_name]["f_PLI"]),
1301
+ np.log10(gw.det[det_name]["PLI"]),
1302
+ 0,
1303
+ alpha=1,
1304
+ color="white",
1305
+ zorder=gw.det[det_name]["zorder"],
1306
+ )
1307
+ if is_existing:
1308
+ ax.fill_between(
1309
+ np.log10(gw.det[det_name]["f_PLI"]),
1310
+ np.log10(gw.det[det_name]["PLI"]),
1311
+ 0,
1312
+ alpha=transparency,
1313
+ color=gw.det[det_name]["color"],
1314
+ zorder=gw.det[det_name]["zorder"],
1315
+ label=gw.det[det_name]["plot_name"],
1316
+ )
1317
+ else:
1318
+ ax.fill_between(
1319
+ np.log10(gw.det[det_name]["f_PLI"]),
1320
+ np.log10(gw.det[det_name]["PLI"]),
1321
+ 0,
1322
+ alpha=0,
1323
+ color="none",
1324
+ hatch="\\\\",
1325
+ edgecolor=gw.det[det_name]["color"],
1326
+ linewidth=0.0,
1327
+ zorder=gw.det[det_name]["zorder"],
1328
+ label=gw.det[det_name]["plot_name"],
1329
+ )
1330
+
1331
+
1332
+
1333
+ add_violins(ax, violincolor="#C0105A", nfreq=nfreq)
1334
+ add_NeffHatch(ax, Neffcolor="grey", h2OmegaNeff=2.48e-7)
1335
+ if not call_from_spectrum:
1336
+ ax.set_title("PLI sensitivities and PTA violins")
1337
+ ax.set_xlabel(R"$f$ / $\mathrm{Hz}$")
1338
+ ax.set_ylabel(R'$h^2 \Omega_\mathrm{GW} (f)$')
1339
+
1340
+ ax.tick_params(axis='both', which='both', direction="in", width=0.5, zorder=1000)
1341
+ ax.xaxis.set_ticks_position('both')
1342
+ ax.yaxis.set_ticks_position('both')
1343
+ for axis in ["top", 'bottom','left','right']:
1344
+ ax.spines[axis].set_linewidth(0.5)
1345
+ ax.spines[axis].set_zorder(10000)
1346
+
1347
+ xlim = [-10,-6]
1348
+ xa = np.linspace(int(xlim[0]), int(xlim[1]), int(xlim[1] - xlim[0] + 1))
1349
+ xtMajor = [j for j in xa]
1350
+ xtMinor = [np.log10(i*10**j) for j in xa for i in range(10)[1:10]]
1351
+ label_map = {-9: "nHz", -6: "µHz", -3: "mHz", 0: "Hz", 3: "kHz"}
1352
+ xlMajor = [label_map.get(i, "") for i in xa]
1353
+ xMajorLocator = FixedLocator(xtMajor)
1354
+ xMinorLocator = FixedLocator(xtMinor)
1355
+ xMajorFormatter = FixedFormatter(xlMajor)
1356
+ ax.xaxis.set_major_locator(xMajorLocator)
1357
+ ax.xaxis.set_minor_locator(xMinorLocator)
1358
+ ax.xaxis.set_major_formatter(xMajorFormatter)
1359
+ ax.set_xlim(-9.5, -5.5)
1360
+
1361
+ ylim = [-18, -6]
1362
+ ya = np.linspace(ylim[0], ylim[1], int(ylim[1] - ylim[0] + 1))
1363
+ ytMajor = [j for j in ya]
1364
+ ytMinor = [np.log10(i*10**j) for i in range(10)[1:10] for j in ya]
1365
+ ylMajor = [r"$10^{" + str(int(i)) + "}$" if i%2==1 else "" for i in ya]
1366
+ yMajorLocator = FixedLocator(ytMajor)
1367
+ yMinorLocator = FixedLocator(ytMinor)
1368
+ yMajorFormatter = FixedFormatter(ylMajor)
1369
+ ax.yaxis.set_major_locator(yMajorLocator)
1370
+ ax.yaxis.set_minor_locator(yMinorLocator)
1371
+ ax.yaxis.set_major_formatter(yMajorFormatter)
1372
+ ax.set_ylim(ylim[0], ylim[1])
1373
+ ax.yaxis.set_zorder(1000)
1374
+ ax.xaxis.set_zorder(1000)
1375
+ ax.set_ylim(-16, -6)
1376
+
1377
+ #ax.grid()
1378
+ add_TL_logo(loc="outside", ax=ax, small_logo=False)
1379
+ #fig.legend(loc="lower right", bbox_to_anchor=(1.0, 0.7), ncol=1)
1380
+ if not call_from_spectrum:
1381
+ plt.tight_layout()
1382
+ if showplot:
1383
+ plt.show()
1384
+ else:
1385
+ from pathlib import Path
1386
+ Path(gw.foldername).mkdir(parents=True, exist_ok=True)
1387
+ plt.savefig(gw.foldername+"GW_sensitivities.pdf")
1388
+ plt.close(fig)
1389
+ else:
1390
+ return fig, ax
1391
+
1392
+ def plotSpectrum(gw, showplot : bool=True):
1393
+ """Plot the total spectrum and its components together with sensitivities.
1394
+
1395
+ Parameters
1396
+ ----------
1397
+ gw : GWSpectrum
1398
+ The GWSpectrum object containing the spectrum data.
1399
+ showplot : bool, optional
1400
+ Whether to display the plot.
1401
+ """
1402
+ fig, ax = plotSensitivities(gw, showplot=False, call_from_spectrum=True)
1403
+ frequencies_Hz = np.logspace(-10, 4, 1000) # Hz
1404
+ sourcecolors = [DESYcyan, DESYorange, DESYmagenta]
1405
+ with np.errstate(divide='ignore'):
1406
+ for i, source in enumerate(gw.fopt_spectrum.sources):
1407
+ ax.plot(np.log10(frequencies_Hz), np.log10(gw.my_spec(
1408
+ frequencies_Hz, source)), label=source, color=sourcecolors[i], zorder=100+i)
1409
+ ax.plot(np.log10(frequencies_Hz), np.log10(gw.my_spec(
1410
+ frequencies_Hz)), label="Total", color="black", ls="dashed", zorder=100+i+1)
1411
+
1412
+ fig.tight_layout()
1413
+ ax.legend(bbox_to_anchor=(1, 0.7), ncol=1)
1414
+
1415
+ if showplot:
1416
+ plt.show()
1417
+ else:
1418
+ from pathlib import Path
1419
+ Path(gw.foldername).mkdir(parents=True, exist_ok=True)
1420
+ plt.savefig(gw.foldername+"GW_spectrum.pdf")
1421
+ plt.close(fig)
1422
+
1423
+
1424
+ def plotProfile2(transitions, conversionFactor):
1425
+ """Standalone function to plot the bubble profile.
1426
+
1427
+ Parameters
1428
+ ----------
1429
+ transitions : list[TransitionInfo]
1430
+ List of the transition objects
1431
+ conversionFactor : float
1432
+ Conversion factor to go from internal units to GeV
1433
+ Returns
1434
+ -------
1435
+ None."""
1436
+ fopts = [tr for tr in transitions if getattr(tr, "type", None) == 1]
1437
+ if not fopts:
1438
+ console.print("[bold yellow]No first-order transitions available for profile plot.[/bold yellow]")
1439
+ return
1440
+
1441
+ height = 3.0
1442
+ fig, axes = plt.subplots(len(fopts), 1, figsize=(6.4, height * len(fopts)))
1443
+ if len(fopts) == 1:
1444
+ axes = [axes]
1445
+ for ax in axes:
1446
+ ax.tick_params(axis="both", which="both", labelsize=10, direction="in", width=0.5)
1447
+ ax.xaxis.set_ticks_position("both")
1448
+ ax.yaxis.set_ticks_position("both")
1449
+ for spine in ["top", "bottom", "left", "right"]:
1450
+ ax.spines[spine].set_linewidth(0.5)
1451
+ ylabel = r"$\phi-\phi_\mathrm{min}^\mathrm{false}$ / $\mathrm{GeV}$"
1452
+ for ax, tr in zip(axes, fopts):
1453
+ instanton = tr.instanton
1454
+ Phis = instanton.Phi
1455
+ for i in range(Phis.shape[-1]):
1456
+ lbl = r"$\phi_" + f"{i}" + r"$"
1457
+ ax.plot(
1458
+ instanton.profile1D.R / conversionFactor,
1459
+ Phis[...,i] * conversionFactor, label=lbl,
1460
+ )
1461
+ ax.set_ylabel(ylabel)
1462
+ tnuc_gev = tr.Tnuc * conversionFactor
1463
+ prefix = r"T_\mathrm{nuc}"
1464
+ if tnuc_gev < 1e-2 or tnuc_gev > 1e4:
1465
+ exponent = int(np.floor(np.log10(tnuc_gev)))
1466
+ mantissa = tnuc_gev / 10 ** exponent
1467
+ label = rf"{prefix} = {mantissa:.3f} \cdot 10^{{{exponent}}}\,\mathrm{{GeV}}"
1468
+ else:
1469
+ label = rf"{prefix} = {tnuc_gev:g}\,\mathrm{{GeV}}"
1470
+ ax.set_title(
1471
+ fr"Bounce at ${label}$ from phase {tr.high_phase} to {tr.low_phase}"
1472
+ )
1473
+ axes[-1].set_xlabel(r"Bubble radius / $\mathrm{GeV}^{-1}$")
1474
+ add_TL_logo(loc="lower right", magnification=2, ax=axes[-1])
1475
+ fig.tight_layout()
1476
+ plt.legend()
1477
+ # fig.savefig(self.output_folder_name + "profile.pdf")
1478
+ plt.show()
1479
+
1480
+ def plotGWSpectrum(gwparams_dict: dict, showplot: bool=False, saveplot: bool=True,
1481
+ foldername: str="", filename: str="", fig=None, ax=None,
1482
+ legendfontsize: int=8):
1483
+ """Build the spectrum from parameters and plot it for the core detectors.
1484
+
1485
+ Parameters
1486
+ ----------
1487
+ gwparams_dict : dict
1488
+ Dictionary containing the parameters for the GW spectrum.
1489
+ showplot : bool, optional
1490
+ Whether to display the plot.
1491
+ foldername : str, optional
1492
+ Folder to save the plot if showplot is False.
1493
+ filename : str, optional
1494
+ Filename to save the plot if showplot is False.
1495
+
1496
+ """
1497
+ astroparams_dict = {}
1498
+ gwspec = FOPTspectrum(gwparams_dict, astroparams_dict, verbose=False)
1499
+ gw = Observability(gwspec, verbose=False, include_smbhb=False)
1500
+ figwasnone = False
1501
+ if fig == None:
1502
+ figwasnone = True
1503
+ if ax == None:
1504
+ fig, ax = plotSensitivities(gw, showplot=False, call_from_spectrum=True, fig=fig, ax=ax)
1505
+ frequencies_Hz = np.logspace(-10, 4, 1000) # Hz
1506
+ sourcecolors = [DESYcyan, DESYorange, DESYmagenta]
1507
+ with np.errstate(divide='ignore'):
1508
+ for i, source in enumerate(gwspec.sources):
1509
+ ax.plot(np.log10(frequencies_Hz), np.log10(gw.my_spec(
1510
+ frequencies_Hz, source)), label=source, color=sourcecolors[i], zorder=100+i)
1511
+ ax.plot(np.log10(frequencies_Hz), np.log10(gw.my_spec(
1512
+ frequencies_Hz)), label="Total", color="black", ls="dashed", zorder=100+i+1)
1513
+
1514
+ if figwasnone:
1515
+ fig.tight_layout()
1516
+ ax.legend(bbox_to_anchor=(1.05, 0.6), ncol=1, fontsize=legendfontsize)
1517
+
1518
+ if showplot:
1519
+ plt.show()
1520
+ plt.close(fig)
1521
+ elif saveplot:
1522
+ from pathlib import Path
1523
+ Path(foldername).mkdir(parents=True, exist_ok=True)
1524
+ if filename != "":
1525
+ plt.savefig(foldername+filename)
1526
+ else:
1527
+ plt.savefig(foldername+"GW_spectrum.pdf")
1528
+ plt.close(fig)
1529
+ else:
1530
+ return fig, ax
1531
+
1532
+
1533
+ def plotSpectrum2PTA(gwparams_dict : dict, showplot : bool=True,
1534
+ foldername : str="", filename : str="", title : str=""):
1535
+ """Build the spectrum and plot it together with PTA sensitivities."""
1536
+ astroparams_dict = {}
1537
+ gwspec = FOPTspectrum(gwparams_dict, astroparams_dict, verbose=False)
1538
+ gw = Observability(gwspec, verbose=False, include_smbhb=False)
1539
+ fig, ax = plotSensitivitiesPTA(gw, showplot=False, call_from_spectrum=True, nfreq=14)
1540
+ frequencies_Hz = np.logspace(-10, -5, 1000) # Hz
1541
+ sourcecolors = [DESYcyan, DESYorange, DESYmagenta]
1542
+ with np.errstate(divide='ignore'):
1543
+ for i, source in enumerate(gwspec.sources):
1544
+ ax.plot(np.log10(frequencies_Hz), np.log10(gw.my_spec(
1545
+ frequencies_Hz, source)), label=source, color=sourcecolors[i], zorder=100+i)
1546
+ ax.plot(np.log10(frequencies_Hz), np.log10(gw.my_spec(
1547
+ frequencies_Hz)), label="Total", color="black", ls="dashed", zorder=100+i+1)
1548
+
1549
+ fig.tight_layout()
1550
+ fig.legend(bbox_to_anchor=(0.99, 0.55), ncol=1, fontsize=7)
1551
+ fig.suptitle(title)
1552
+ fig.subplots_adjust(top=0.92)
1553
+ if showplot:
1554
+ plt.show()
1555
+ else:
1556
+ from pathlib import Path
1557
+ Path(foldername).mkdir(parents=True, exist_ok=True)
1558
+ if filename != "":
1559
+ plt.savefig(foldername+filename)
1560
+ else:
1561
+ plt.savefig(foldername+"GW_spectrum_PTA.pdf")
1562
+ plt.close(fig)
1563
+
1564
+ def plot2d(pot : generic_potential, box : tuple, ax : Optional[plt.Axes] = None,
1565
+ T : float = 0, treelevel : bool = False, offset : float = 0,
1566
+ xaxis : int = 0, yaxis : int = 1, n : int = 50, clevs : int = 200,
1567
+ cfrac : float = .8, log : bool = False, **contourParams):
1568
+ """
1569
+ Makes a countour plot of the potential.
1570
+
1571
+ Parameters
1572
+ ----------
1573
+ box : tuple
1574
+ The bounding box for the plot, (xlow, xhigh, ylow, yhigh).
1575
+ T : float, optional
1576
+ The temperature
1577
+ offset : array_like
1578
+ A constant to add to all coordinates. Especially
1579
+ helpful if Ndim > 2.
1580
+ xaxis, yaxis : int, optional
1581
+ The integers of the axes that we want to plot.
1582
+ n : int
1583
+ Number of points evaluated in each direction.
1584
+ clevs : int
1585
+ Number of contour levels to draw.
1586
+ cfrac : float
1587
+ The lowest contour is always at ``min(V)``, while the highest is
1588
+ at ``min(V) + cfrac*(max(V)-min(V))``. If ``cfrac < 1``, only part
1589
+ of the plot will be covered. Useful when the minima are more
1590
+ important to resolve than the maximum.
1591
+ log : bool, optional
1592
+ Log scale of the colorbar.
1593
+ contourParams :
1594
+ Any extra parameters to be passed to :func:`plt.contour`.
1595
+ """
1596
+ xmin, xmax, ymin, ymax = box
1597
+
1598
+ if ax is None:
1599
+ fig = plt.figure(figsize=(5.31,3), edgecolor="white")
1600
+ ax = fig.add_subplot(1,1,1)
1601
+ ax.tick_params(axis='both', which='both', labelsize=10, direction="in", width=0.5)
1602
+ ax.xaxis.set_ticks_position('both')
1603
+ ax.yaxis.set_ticks_position('both')
1604
+ for axis in ['top','bottom','left','right']:
1605
+ ax.spines[axis].set_linewidth(0.5)
1606
+ else:
1607
+ fig = ax.figure
1608
+
1609
+ X = np.linspace(xmin, xmax, n).reshape(n, 1) * np.ones((1, n))
1610
+ Y = np.linspace(ymin, ymax, n).reshape(1, n) * np.ones((n, 1))
1611
+ XY = np.zeros((n, n, pot.Ndim))
1612
+ XY[..., xaxis], XY[..., yaxis] = X, Y
1613
+ XY += offset
1614
+ Z = pot.V0(XY) if treelevel else pot.DVtot(XY, T)
1615
+ X_plot = X * pot.conversionFactor
1616
+ Y_plot = Y * pot.conversionFactor
1617
+ Z_plot = Z * pot.conversionFactor**4
1618
+ if log:
1619
+ Z_plot = np.log10(Z_plot - np.min(Z_plot) + 1)
1620
+ minZ, maxZ = min(Z_plot.ravel()), max(Z_plot.ravel())
1621
+ N = np.linspace(minZ, minZ + (maxZ - minZ) * cfrac, clevs)
1622
+ contour = ax.contour(X_plot, Y_plot, Z_plot, N, cmap=cmapTL2, **contourParams)
1623
+ cbar = fig.colorbar(contour, ax=ax)
1624
+ if log:
1625
+ cbar.set_label(r"$log_10(V - V_{\mathrm{min}} + 1)$")
1626
+ else:
1627
+ cbar.set_label(r"$V / \mathrm{GeV}^4$")
1628
+ ax.set_xlabel(r"$\phi_1$ / $\mathrm{GeV}$")
1629
+ ax.set_ylabel(r"$\phi_2$ / $\mathrm{GeV}$")
1630
+ ax.yaxis.set_zorder(1000)
1631
+ ax.xaxis.set_zorder(1000)
1632
+ ax.set_xlim(xmin*pot.conversionFactor, xmax*pot.conversionFactor)
1633
+ ax.set_ylim(ymin*pot.conversionFactor, ymax*pot.conversionFactor)
1634
+ # Matplotlib compatibility: avoid switching layout engines when the figure
1635
+ # already uses one (e.g. constrained layout).
1636
+ if fig.get_layout_engine() is None:
1637
+ fig.tight_layout()
1638
+ return fig, ax
1639
+
1640
+
1641
+ def plot1d(pot : generic_potential, x1 : float, x2 : float, ax : Optional[plt.Axes] = None,
1642
+ T : float = 0, treelevel : bool = False, subtract : bool = True, n : int = 500,
1643
+ **plotParams):
1644
+ """Plot the 1D projection of the potential between two field values.
1645
+
1646
+ Parameters
1647
+ ----------
1648
+ x1 : float
1649
+ The starting field value.
1650
+ x2 : float
1651
+ The ending field value.
1652
+ T : float, optional
1653
+ The temperature.
1654
+ treelevel : bool, optional
1655
+ Whether to plot the tree-level potential or the full one-loop
1656
+ potential including thermal corrections.
1657
+ subtract : bool, optional
1658
+ Whether to subtract the potential at zero field value.
1659
+ n : int, optional
1660
+ Number of points to evaluate.
1661
+ plotParams :
1662
+ Any extra parameters to be passed to :func:`plt.plot`.
1663
+ """
1664
+ if pot.Ndim == 1:
1665
+ x = np.linspace(x1, x2, n)
1666
+ X = x[:, np.newaxis]
1667
+ else:
1668
+ dX = np.array(x2) - np.array(x1)
1669
+ X = dX * np.linspace(0, 1, n)[:, np.newaxis] + x1
1670
+ x = np.linspace(0, 1, n) * np.sum(dX**2)**.5
1671
+ if treelevel:
1672
+ y = pot.V0(X) - pot.V0(X * 0) if subtract else pot.V0(X)
1673
+ else:
1674
+ y = pot.DVtot(X, T) if subtract else pot.Vtot(X, T)
1675
+
1676
+ if ax is None:
1677
+ fig = plt.figure(figsize=(5.31,3), edgecolor="white")
1678
+ ax = fig.add_subplot(1,1,1)
1679
+ ax.tick_params(axis='both', which='both', labelsize=10, direction="in", width=0.5)
1680
+ ax.xaxis.set_ticks_position('both')
1681
+ ax.yaxis.set_ticks_position('both')
1682
+ for axis in ['top','bottom','left','right']:
1683
+ ax.spines[axis].set_linewidth(0.5)
1684
+
1685
+ ax.plot(x * pot.conversionFactor, y * pot.conversionFactor**4, **plotParams)
1686
+ ax.set_xlabel(R"$\phi$ / $\mathrm{GeV}$")
1687
+ ax.set_ylabel(R"$V(\phi) / \mathrm{GeV}^4$")
1688
+ ax.yaxis.set_zorder(1000)
1689
+ ax.xaxis.set_zorder(1000)
1690
+ ax.set_xlim(x1*pot.conversionFactor, x2*pot.conversionFactor)
1691
+ fig.tight_layout()
1692
+ return fig, ax
1693
+
1694
+ def add_violins(ax : plt.Axes, violincolor : str = DESYdunkelrot, nfreq : int = 14):
1695
+ """Add stylistic violin markers to highlight PTA frequency bins.
1696
+
1697
+ Parameters
1698
+ ----------
1699
+ ax : plt.Axes
1700
+ The axes to add the violins to.
1701
+ violincolor : str, optional
1702
+ The color of the violins.
1703
+ nfreq : int, optional
1704
+ Number of frequency bins for the violins. Default is 14.
1705
+ """
1706
+ directory = DATA_DIR / "30f_fs{hd}_100fDMGP_ceffyl"
1707
+
1708
+ def log10_h2O(log10_rho, f, T):
1709
+ return np.log10(8 * np.pi**4 / cn.H100_Hz**2 * 10**(2 * log10_rho) * f**5 * T)
1710
+
1711
+ N_violin = int(1e5)
1712
+ density = np.load(directory / 'density.npy')
1713
+ freqs = np.load(directory / 'freqs.npy')
1714
+ log10rhogrid = np.load(directory / 'log10rhogrid.npy')
1715
+
1716
+ for i in range(nfreq):
1717
+ samplebin = np.array(
1718
+ log10_h2O(
1719
+ _choice(
1720
+ a=log10rhogrid,
1721
+ p=np.exp(density[0,i,:])/np.sum(np.exp(density[0,i,:])),
1722
+ size=N_violin
1723
+ ),
1724
+ freqs[i],
1725
+ 1/freqs[0]
1726
+ )
1727
+ )
1728
+
1729
+ violin_parts = ax.violinplot(dataset=(samplebin),
1730
+ positions=[np.log10(freqs[i])]
1731
+ , widths=[np.log10(freqs[i])*.01], showextrema=False)
1732
+ for pc in violin_parts['bodies']:
1733
+ pc.set(color=violincolor, edgecolor=None, alpha=1, zorder=100)
1734
+ if i == 0:
1735
+ pc.set(label="NG15 (violins)")
1736
+
1737
+ def add_NeffHatch(ax : plt.Axes, Neffcolor : str = DESYpetrol, h2OmegaNeff: float = 2.48e-7):
1738
+ """Overlay a hatched region indicating the :math:`\\Delta N_\\mathrm{eff}` bound.
1739
+
1740
+ Parameters
1741
+ ----------
1742
+ ax : matplotlib.axes.Axes
1743
+ The axes to add the hatch to.
1744
+ Neffcolor : str, optional
1745
+ The color of the hatch.
1746
+ h2OmegaNeff : float, optional
1747
+ The upper bound on :math:`h^2 \\Omega_\\mathrm{GW}` from :math:`\\Delta N_\\mathrm{eff}`.
1748
+ Default is ``2.48e-7``.
1749
+ """
1750
+ fNeff = 1.5e-11
1751
+ frequencies_Hz = np.linspace(-12, 5, 3) # Hz
1752
+ ax.hlines(y=np.log10(h2OmegaNeff), xmin=np.log10(fNeff), xmax=20, linewidth=1, color=Neffcolor, zorder=20)
1753
+ ax.fill_between(
1754
+ frequencies_Hz,
1755
+ np.ones_like(frequencies_Hz) * np.log10(h2OmegaNeff),
1756
+ 1,
1757
+ color="white",
1758
+ linewidth=0.0,
1759
+ zorder=10,
1760
+ alpha=1,
1761
+ )
1762
+ ax.fill_between(
1763
+ frequencies_Hz,
1764
+ np.ones_like(frequencies_Hz) * np.log10(h2OmegaNeff),
1765
+ 1,
1766
+ color=Neffcolor,
1767
+ linewidth=0.0,
1768
+ zorder=10,
1769
+ alpha=0.5,
1770
+ )
1771
+ ax.fill_between(
1772
+ frequencies_Hz,
1773
+ np.ones_like(frequencies_Hz) * np.log10(h2OmegaNeff),
1774
+ 1,
1775
+ color="none",
1776
+ hatch="//",
1777
+ edgecolor=Neffcolor,
1778
+ linewidth=0.0,
1779
+ zorder=10,
1780
+ label=R"$N_\mathrm{eff}$",
1781
+ )