TransitionListener 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transitionlistener/__init__.py +21 -0
- transitionlistener/bubbledynamics.py +2250 -0
- transitionlistener/bubbledynamics_fixedstep.py +2929 -0
- transitionlistener/check_installation.py +309 -0
- transitionlistener/ckm.py +91 -0
- transitionlistener/colors.py +288 -0
- transitionlistener/config.py +185 -0
- transitionlistener/constants.py +64 -0
- transitionlistener/counterterms/__init__.py +230 -0
- transitionlistener/counterterms/cw.py +298 -0
- transitionlistener/counterterms/tensors.py +343 -0
- transitionlistener/counterterms/twohdm.py +520 -0
- transitionlistener/counterterms/twohdm_solver.py +374 -0
- transitionlistener/errors.py +315 -0
- transitionlistener/examples/bp_et.yaml +64 -0
- transitionlistener/examples/bp_lisa.yaml +70 -0
- transitionlistener/examples/bp_muares.yaml +69 -0
- transitionlistener/examples/bp_pta.yaml +64 -0
- transitionlistener/examples/example_dark_photon.yaml +73 -0
- transitionlistener/examples/example_grid.yaml +55 -0
- transitionlistener/examples/example_line.yaml +51 -0
- transitionlistener/examples/example_point.yaml +73 -0
- transitionlistener/examples/example_random_conformal.yaml +42 -0
- transitionlistener/examples/example_ultranest.yaml +39 -0
- transitionlistener/finiteT.py +468 -0
- transitionlistener/generic_potential.py +1521 -0
- transitionlistener/gridplots.py +592 -0
- transitionlistener/gwfopt.py +604 -0
- transitionlistener/helper_functions.py +843 -0
- transitionlistener/hydrodynamics.py +647 -0
- transitionlistener/interface/__init__.py +95 -0
- transitionlistener/interface/check.py +51 -0
- transitionlistener/interface/cli.py +211 -0
- transitionlistener/interface/config_loader.py +264 -0
- transitionlistener/interface/logging_utils.py +30 -0
- transitionlistener/interface/output_schema.py +296 -0
- transitionlistener/interface/pipeline.py +300 -0
- transitionlistener/interface/samplers.py +565 -0
- transitionlistener/interface/scans.py +468 -0
- transitionlistener/interface/single_point.py +581 -0
- transitionlistener/interface/state.py +61 -0
- transitionlistener/lineplots.py +597 -0
- transitionlistener/logo/TL-logo_large.png +0 -0
- transitionlistener/logo/TL-logo_small.png +0 -0
- transitionlistener/models/TL_2HDM.py +512 -0
- transitionlistener/models/TL_conformal_dark_u1.py +202 -0
- transitionlistener/models/TL_dark_U1.py +198 -0
- transitionlistener/models/TL_dark_U1_g_parameterization.py +198 -0
- transitionlistener/models/TL_dark_flipflop.py +274 -0
- transitionlistener/models/templatePotential.py +260 -0
- transitionlistener/nucleation.py +412 -0
- transitionlistener/observability.py +790 -0
- transitionlistener/particles.py +436 -0
- transitionlistener/pathDeformation.py +1462 -0
- transitionlistener/percolation_adaptive_gridbuilders.py +1385 -0
- transitionlistener/percolation_adaptive_rate.py +1049 -0
- transitionlistener/percolation_adaptivestepsize.py +1632 -0
- transitionlistener/percolation_fixedstepsize.py +618 -0
- transitionlistener/phases.py +1499 -0
- transitionlistener/plot_settings.py +121 -0
- transitionlistener/plots.py +1781 -0
- transitionlistener/runtime_options.py +278 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/bandwidths.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/density.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/freqs.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log.txt +16 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rhogrid.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rholabels.txt +30 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/pulsar_list.txt +1 -0
- transitionlistener/tab_data/HLV-O2-PLI.csv +82 -0
- transitionlistener/tab_data/HLV-O2-noise.csv +83 -0
- transitionlistener/tab_data/HLVK-design-PLI.csv +61 -0
- transitionlistener/tab_data/HLVK-design-noise.csv +83 -0
- transitionlistener/tab_data/NG15_PLI_hc.csv +31 -0
- transitionlistener/tab_data/NG15_noise_hc.csv +56 -0
- transitionlistener/tab_data/PLI-dataset.csv +327 -0
- transitionlistener/tab_data/energydensities.dat +1001 -0
- transitionlistener/tab_data/finiteT_b.dat.txt +1999 -0
- transitionlistener/tab_data/finiteT_f.dat.txt +1000 -0
- transitionlistener/tab_data/geff_SM.dat +10009 -0
- transitionlistener/tab_data/muAres_f_Hz_PLI_h2OmegaGW.csv +131 -0
- transitionlistener/tab_data/muAres_f_Hz_h2Omega_noise.csv +99 -0
- transitionlistener/tab_data/noise-dataset.csv +327 -0
- transitionlistener/tab_data/numberdensities.dat +1001 -0
- transitionlistener/tab_data/pressures.dat +1001 -0
- transitionlistener/thermodynamics.py +686 -0
- transitionlistener/transitionObservables.py +1199 -0
- transitionlistener/transitionObservables_fixedstep.py +976 -0
- transitionlistener/transitions.py +1000 -0
- transitionlistener/tunneling1D.py +910 -0
- transitionlistener-2.0.0.dist-info/METADATA +817 -0
- transitionlistener-2.0.0.dist-info/RECORD +95 -0
- transitionlistener-2.0.0.dist-info/WHEEL +4 -0
- transitionlistener-2.0.0.dist-info/entry_points.txt +2 -0
- transitionlistener-2.0.0.dist-info/licenses/LICENSE.txt +674 -0
|
@@ -0,0 +1,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
|
+
)
|