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,1521 @@
|
|
|
1
|
+
"""The primary task of the generic_potential module is to define the
|
|
2
|
+
:class:`generic_potential` class, from which realistic scalar field models can
|
|
3
|
+
straightforwardly be constructed. The most important part of any such model is,
|
|
4
|
+
appropriately, the potential function and its gradient. This present module is not
|
|
5
|
+
necessary to define a potential, but it does make the process somewhat simpler
|
|
6
|
+
by automatically calculating one-loop effects from a model-specific mass
|
|
7
|
+
spectrum, constructing numerical derivative functions, providing a
|
|
8
|
+
simplified interface to the :mod:`.transitionFinder` module, and providing
|
|
9
|
+
several methods for plotting the potential and its phases.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
The generic_potential class has to provide the following information to the
|
|
13
|
+
rest of the code:
|
|
14
|
+
|
|
15
|
+
1. Vtot, dVdx, dVdT, d2Vdx (Potential)
|
|
16
|
+
2. energyDensity (the total energy density at a given temperature)
|
|
17
|
+
3. configuration
|
|
18
|
+
4. Tmax, Tmin (range of validitiy of the potential)
|
|
19
|
+
5. conversionFactor (conversion between internal units and GeV)
|
|
20
|
+
6. X0 (zero temperature global minimum)
|
|
21
|
+
7. Mass spectrum of the theory.
|
|
22
|
+
|
|
23
|
+
Part of TransitionListener v2.0
|
|
24
|
+
Documentation: https://tasillo.de/TransitionListener/
|
|
25
|
+
|
|
26
|
+
Authors:
|
|
27
|
+
Jonas Matuszak <jonas.matuszak@kit.edu>
|
|
28
|
+
Carlo Tasillo <carlo.tasillo@ific.uv.es>
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
from scipy import optimize
|
|
33
|
+
from rich.columns import Columns as RichColumns
|
|
34
|
+
import rich
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
from transitionlistener.finiteT import Jb_spline as Jb
|
|
38
|
+
from transitionlistener.finiteT import Jf_spline as Jf
|
|
39
|
+
from transitionlistener import helper_functions
|
|
40
|
+
from transitionlistener.thermodynamics import e_geffSM, p_geffSM, set_sm_temperature_cap
|
|
41
|
+
|
|
42
|
+
from transitionlistener.bubbledynamics import approxNucleationCriterion
|
|
43
|
+
|
|
44
|
+
from transitionlistener import errors
|
|
45
|
+
from transitionlistener import runtime_options
|
|
46
|
+
from transitionlistener.config import Configuration
|
|
47
|
+
from transitionlistener import constants as cn
|
|
48
|
+
from transitionlistener.counterterms.cw import coleman_weinberg_derivatives as _cw_derivatives
|
|
49
|
+
|
|
50
|
+
from . import console
|
|
51
|
+
from transitionlistener.particles import (
|
|
52
|
+
MassSpectrum,
|
|
53
|
+
SpectrumSnapshot,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class generic_potential():
|
|
58
|
+
"""
|
|
59
|
+
An abstract class from which one can easily create finite-temperature
|
|
60
|
+
effective potentials.
|
|
61
|
+
|
|
62
|
+
This class acts as the skeleton around which different scalar field models
|
|
63
|
+
can be formed. At a bare minimum, subclasses must implement :func:`init`,
|
|
64
|
+
:func:`V0`, and they must provide a
|
|
65
|
+
:class:`~transitionlistener.particles.MassSpectrum`
|
|
66
|
+
instance via ``self.mass_spectrum``. Subclasses will also likely implement
|
|
67
|
+
:func:`approxZeroTMin`. Once the tree-level
|
|
68
|
+
potential and particle spectrum are defined, the one-loop zero-temperature
|
|
69
|
+
potential (using MS-bar renormalization) and finite-temperature potential
|
|
70
|
+
can be used without any further modification.
|
|
71
|
+
|
|
72
|
+
If one wishes to rewrite the effective potential from scratch (that is,
|
|
73
|
+
using a different method to calculate one-loop and finite-temperature
|
|
74
|
+
corrections), this class and its various helper functions can still be used.
|
|
75
|
+
In that case, one would need to override :func:`Vtot` (used by most of the
|
|
76
|
+
helper functions) and :func:`V1T_from_X` (which should only return the
|
|
77
|
+
temperature-dependent part of Vtot; used in temperature derivative
|
|
78
|
+
calculations), and possibly override :func:`V0` (used by
|
|
79
|
+
:func:`massSqMatrix` and for plotting at tree level).
|
|
80
|
+
|
|
81
|
+
The `__init__` function performs initialization specific for this abstract
|
|
82
|
+
class. Subclasses should either override this initialization *but make sure
|
|
83
|
+
to call the parent implementation*, or, more simply, override the
|
|
84
|
+
:func:`init` method. In the base implementation, the former calls the latter
|
|
85
|
+
and the latter does nothing. At a bare minimum, subclasses must set the
|
|
86
|
+
`Ndim` parameter to the number of dynamic field dimensions in the model.
|
|
87
|
+
|
|
88
|
+
One of the main jobs of this class is to provide an easy interface for
|
|
89
|
+
calculating the phase structure and phase transitions. These are given by
|
|
90
|
+
the methods :func:`getPhases`, :func:`calcTcTrans`, and
|
|
91
|
+
:func:`findAllTransitions`.
|
|
92
|
+
|
|
93
|
+
The following attributes can (and should!) be set during initialiation:
|
|
94
|
+
|
|
95
|
+
Attributes
|
|
96
|
+
----------
|
|
97
|
+
Ndim : int
|
|
98
|
+
The number of dynamic field dimensions in the model. This *must* be
|
|
99
|
+
overridden by subclasses during initialization.
|
|
100
|
+
x_eps : float
|
|
101
|
+
The epsilon to use in brute-force evalutations of the gradient and
|
|
102
|
+
for the second derivatives. May be overridden by subclasses;
|
|
103
|
+
defaults to 0.001.
|
|
104
|
+
T_eps : float
|
|
105
|
+
The epsilon to use in brute-force evalutations of the temperature
|
|
106
|
+
derivative. May be overridden by subclasses; defaults to 0.001.
|
|
107
|
+
deriv_order : int
|
|
108
|
+
Sets the order to which finite difference derivatives are calculated.
|
|
109
|
+
Must be 2 or 4. May be overridden by subclasses; defaults to 4.
|
|
110
|
+
renormScaleSq : float
|
|
111
|
+
The square of the renormalization scale to use in the MS-bar one-loop
|
|
112
|
+
zero-temp potential. May be overridden by subclasses;
|
|
113
|
+
defaults to 1000.0**2 in internal units.
|
|
114
|
+
Tmax : float
|
|
115
|
+
The maximum temperature to which minima should be followed. No
|
|
116
|
+
transitions are calculated above this temperature. This is also used
|
|
117
|
+
as the overall temperature scale in :func:`getPhases`.
|
|
118
|
+
May be overridden by subclasses; defaults to 1000.0 in internal units.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, *args, **dargs) -> None:
|
|
122
|
+
"""Prepare configuration objects and delegate to :meth:`init` for setup."""
|
|
123
|
+
|
|
124
|
+
self.config = Configuration() # This contains all the tracing settings, etc
|
|
125
|
+
|
|
126
|
+
self.kin_coupled_e_geff = e_geffSM # This can be overwritten
|
|
127
|
+
self.kin_coupled_p_geff = p_geffSM # This can be overwritten
|
|
128
|
+
|
|
129
|
+
self.kin_decoupled_e_geff = lambda T, cf: 0.0 * T # If the SM is decoupled, put e_geffSM here
|
|
130
|
+
self.kin_decoupled_p_geff = lambda T, cf: 0.0 * T # If the SM is decoupled, put p_geffSM here
|
|
131
|
+
|
|
132
|
+
# The parameters below need to be specified in subclass init()
|
|
133
|
+
self.verbose = False
|
|
134
|
+
self.v_GeV = None
|
|
135
|
+
self.X0 = None
|
|
136
|
+
self.renormScaleSq = None
|
|
137
|
+
self.conversionFactor = None
|
|
138
|
+
self.daisy = None
|
|
139
|
+
self.mass_spectrum: MassSpectrum | None = None
|
|
140
|
+
self.derived_parameters = {}
|
|
141
|
+
|
|
142
|
+
self.setConfigParameters()
|
|
143
|
+
_pre_override_fRC = self.config.tracingConf.tunneling_params[
|
|
144
|
+
"deformation_deform_params"
|
|
145
|
+
]["fRatioConv"]
|
|
146
|
+
args, dargs = runtime_options.apply_input_overrides(self, args, dargs)
|
|
147
|
+
self.v_stable = self.config.tracingConf.internal_scale
|
|
148
|
+
self.x_eps = self.config.tracingConf.tracing_field_accuracy
|
|
149
|
+
self.T_eps = self.config.tracingConf.tracing_temp_accuracy
|
|
150
|
+
self.deriv_order = self.config.tracingConf.tracing_derivative_order
|
|
151
|
+
|
|
152
|
+
self.init(*args, **dargs)
|
|
153
|
+
|
|
154
|
+
# For multi-field potentials the path-deformation convergence target
|
|
155
|
+
# needs to be tighter than the 1d default; otherwise the bounce action
|
|
156
|
+
# exhibits 0.1-1 dex jitter that downstream percolation cannot resolve.
|
|
157
|
+
# Only override when the user has not already set fRatioConv themselves.
|
|
158
|
+
if (
|
|
159
|
+
getattr(self, "Ndim", 1) >= 2
|
|
160
|
+
and self.config.tracingConf.tunneling_params["deformation_deform_params"][
|
|
161
|
+
"fRatioConv"
|
|
162
|
+
]
|
|
163
|
+
== _pre_override_fRC
|
|
164
|
+
):
|
|
165
|
+
self.config.tracingConf.tunneling_params["deformation_deform_params"][
|
|
166
|
+
"fRatioConv"
|
|
167
|
+
] = 1e-2
|
|
168
|
+
self._ensure_mass_spectrum()
|
|
169
|
+
self._sync_mass_labels()
|
|
170
|
+
self.Tmin = cn.T0_SM_GeV / self.conversionFactor
|
|
171
|
+
self.Tmax = self.config.tracingConf.Tmax_factor * self.v_stable
|
|
172
|
+
self.checkInitialisation()
|
|
173
|
+
self.generateInvGroupElements()
|
|
174
|
+
self._update_sm_temperature_cap()
|
|
175
|
+
|
|
176
|
+
if self.verbose:
|
|
177
|
+
self.makePrettyDictionaryPrint(self.derived_parameters)
|
|
178
|
+
|
|
179
|
+
def init(self, *args, **dargs) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Subclasses should override this method (not __init__) to do all
|
|
182
|
+
initialization. At a bare minimum, subclasses need to specify the number
|
|
183
|
+
of dimensions in the potential with ``self.Ndim``.
|
|
184
|
+
"""
|
|
185
|
+
# dummy dimensions
|
|
186
|
+
self.Ndim = 1
|
|
187
|
+
|
|
188
|
+
def setConfigParameters(self) -> None:
|
|
189
|
+
"""Change the default config parameters for tracing, hydrodynamics, and GW
|
|
190
|
+
computation.
|
|
191
|
+
|
|
192
|
+
This method should be overwritten by the subclass.
|
|
193
|
+
"""
|
|
194
|
+
# self.config.tracingConf.internal_scale = 1000
|
|
195
|
+
# self.config.tracingConf.Tmax_factor = 2.5
|
|
196
|
+
# ...
|
|
197
|
+
|
|
198
|
+
def set_tracing_params(self) -> None:
|
|
199
|
+
"""Backward-compatible helper to resync cached tracing attributes.
|
|
200
|
+
|
|
201
|
+
Older model implementations still call this after tweaking the config.
|
|
202
|
+
"""
|
|
203
|
+
self.v_stable = self.config.tracingConf.internal_scale
|
|
204
|
+
self.x_eps = self.config.tracingConf.tracing_field_accuracy
|
|
205
|
+
self.T_eps = self.config.tracingConf.tracing_temp_accuracy
|
|
206
|
+
self.deriv_order = self.config.tracingConf.tracing_derivative_order
|
|
207
|
+
|
|
208
|
+
def set_modelparams(self, inputparam_dict: dict) -> dict:
|
|
209
|
+
"""
|
|
210
|
+
Set the model parameters from the inputparam_dict dictionary. Print warnings
|
|
211
|
+
if some parameters are not set and default values are used instead. Also
|
|
212
|
+
check if the parameters are in the allowed ranges.
|
|
213
|
+
"""
|
|
214
|
+
mp = {name: params.copy() for name, params in self.model_parameters.items()}
|
|
215
|
+
|
|
216
|
+
for key in inputparam_dict.keys():
|
|
217
|
+
if key in mp.keys():
|
|
218
|
+
mp[key]["value"] = float(inputparam_dict[key])
|
|
219
|
+
self.model_parameters[key]["value"] = float(inputparam_dict[key])
|
|
220
|
+
else:
|
|
221
|
+
raise errors.InitPotentialError(f"Unknown model parameter: {key}")
|
|
222
|
+
|
|
223
|
+
for key in mp.keys():
|
|
224
|
+
if "value" not in mp[key].keys():
|
|
225
|
+
mp[key]["value"] = mp[key]["default"]
|
|
226
|
+
console.print(
|
|
227
|
+
f"[yellow]Warning: No value given for model parameter {key}. "
|
|
228
|
+
f"Using default value {mp[key]['default']}.[/yellow]"
|
|
229
|
+
)
|
|
230
|
+
for key in mp.keys():
|
|
231
|
+
try:
|
|
232
|
+
value = float(mp[key]["value"])
|
|
233
|
+
except (ValueError, TypeError):
|
|
234
|
+
value = eval(mp[key]["value"])
|
|
235
|
+
|
|
236
|
+
min_val = mp[key].get("min", -np.inf)
|
|
237
|
+
max_val = mp[key].get("max", np.inf)
|
|
238
|
+
if not (min_val <= value <= max_val):
|
|
239
|
+
raise errors.InitPotentialError(
|
|
240
|
+
f"Model parameter {key}={value} is out of range "
|
|
241
|
+
f"[{min_val}, {max_val}]."
|
|
242
|
+
)
|
|
243
|
+
self._update_sm_temperature_cap()
|
|
244
|
+
return mp
|
|
245
|
+
|
|
246
|
+
def computeConversionFactor(self, v_stable: float, v_GeV: float) -> float:
|
|
247
|
+
"""compute the conversionFactor to go from internal units
|
|
248
|
+
to GeV. """
|
|
249
|
+
conversionFactor = v_GeV / self.v_stable
|
|
250
|
+
return conversionFactor
|
|
251
|
+
|
|
252
|
+
def coleman_weinberg_from_curvatures(
|
|
253
|
+
self,
|
|
254
|
+
tensors: dict[str, np.ndarray],
|
|
255
|
+
vev: np.ndarray,
|
|
256
|
+
*,
|
|
257
|
+
scale: float,
|
|
258
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
259
|
+
r"""Evaluate Coleman–Weinberg derivatives from curvature tensors.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
tensors:
|
|
264
|
+
Dictionary containing the Higgs, gauge, and Yukawa curvature tensors
|
|
265
|
+
in the gauge basis. The expected keys are ``H2``, ``H3``, ``H4``,
|
|
266
|
+
``Gauge``, ``Quark``, and ``Lepton``.
|
|
267
|
+
vev:
|
|
268
|
+
Vacuum expectation value vector expressed in the same field basis
|
|
269
|
+
as the tensors (typically the eight-dimensional Higgs basis).
|
|
270
|
+
scale:
|
|
271
|
+
Renormalisation scale :math:`\mu` entering the Coleman–Weinberg
|
|
272
|
+
logarithms.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
(gradient, hessian): ``np.ndarray``
|
|
277
|
+
Coleman–Weinberg first and second derivatives in physical units.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
grad_phys, hess_phys = _cw_derivatives(
|
|
281
|
+
tensors["H2"],
|
|
282
|
+
tensors["H3"],
|
|
283
|
+
tensors["H4"],
|
|
284
|
+
tensors["Gauge"],
|
|
285
|
+
tensors["Quark"],
|
|
286
|
+
tensors["Lepton"],
|
|
287
|
+
vev,
|
|
288
|
+
scale=scale,
|
|
289
|
+
)
|
|
290
|
+
return grad_phys, hess_phys
|
|
291
|
+
|
|
292
|
+
def _ensure_mass_spectrum(self) -> None:
|
|
293
|
+
"""Verify that the subclass provided a MassSpectrum instance."""
|
|
294
|
+
if not isinstance(self.mass_spectrum, MassSpectrum):
|
|
295
|
+
raise errors.InitPotentialError(
|
|
296
|
+
"Model initialisation must assign `self.mass_spectrum` to a "
|
|
297
|
+
"transitionlistener.particles.MassSpectrum instance."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def _sync_mass_labels(self) -> None:
|
|
301
|
+
"""Derive mass labels directly from the particle definitions."""
|
|
302
|
+
if not isinstance(self.mass_spectrum, MassSpectrum):
|
|
303
|
+
self.boson_mass_labels = {"latex": [], "text": []}
|
|
304
|
+
self.fermion_mass_labels = {"latex": [], "text": []}
|
|
305
|
+
return
|
|
306
|
+
self.boson_mass_labels = {
|
|
307
|
+
"latex": self.mass_spectrum.boson_labels("latex"),
|
|
308
|
+
"text": self.mass_spectrum.boson_labels("text"),
|
|
309
|
+
}
|
|
310
|
+
self.fermion_mass_labels = {
|
|
311
|
+
"latex": self.mass_spectrum.fermion_labels("latex"),
|
|
312
|
+
"text": self.mass_spectrum.fermion_labels("text"),
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
def _update_sm_temperature_cap(self) -> None:
|
|
316
|
+
"""Cache the SM temperature cap using the spectrum at the zero-T minimum."""
|
|
317
|
+
if (
|
|
318
|
+
not isinstance(self.mass_spectrum, MassSpectrum)
|
|
319
|
+
or self.conversionFactor is None
|
|
320
|
+
or self.X0 is None
|
|
321
|
+
):
|
|
322
|
+
return
|
|
323
|
+
try:
|
|
324
|
+
bosons = self.boson_massSq(np.asarray(self.X0), 0.0)
|
|
325
|
+
fermions = self.fermion_massSq(np.asarray(self.X0))
|
|
326
|
+
except Exception as e:
|
|
327
|
+
msg = ("Could not evaluate mass spectrum at zero-T minimum "
|
|
328
|
+
"to set SM temperature cap: ") + str(e)
|
|
329
|
+
console.print(f"[yellow]Warning: {msg}[/yellow]")
|
|
330
|
+
return
|
|
331
|
+
set_sm_temperature_cap(
|
|
332
|
+
self.conversionFactor,
|
|
333
|
+
bosons, self.mass_spectrum.is_SM_bosons,
|
|
334
|
+
fermions, self.mass_spectrum.is_SM_fermions,
|
|
335
|
+
verbose=self.verbose,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def get_mass_spectrum(self, X: np.ndarray, T: float | np.ndarray) -> SpectrumSnapshot:
|
|
339
|
+
"""Evaluate bosonic and fermionic spectra at (X, T)."""
|
|
340
|
+
return self.mass_spectrum.evaluate(X, T)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def boson_massSq(self, X, T):
|
|
344
|
+
r"""Return the bosonic mass spectrum :math:`m_i^2(\phi, T)` of the model.
|
|
345
|
+
|
|
346
|
+
Subclasses typically implement the actual mass matrices through
|
|
347
|
+
``self.mass_spectrum``. The returned tuple is forwarded to the one-loop
|
|
348
|
+
and thermal-potential routines, which use the bosonic contribution
|
|
349
|
+
|
|
350
|
+
.. math::
|
|
351
|
+
V_1^{B}(\phi, T=0)
|
|
352
|
+
= \sum_i \frac{n_i\,m_i^4(\phi,0)}{64\pi^2}
|
|
353
|
+
\left[\log\!\left(\frac{m_i^2(\phi,0)}{\mu^2}\right)-c_i\right].
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
X:
|
|
358
|
+
Field configuration :math:`\phi`.
|
|
359
|
+
T:
|
|
360
|
+
Temperature in internal units.
|
|
361
|
+
"""
|
|
362
|
+
return self.mass_spectrum.bosons_massSq(X, T)
|
|
363
|
+
|
|
364
|
+
def fermion_massSq(self, X):
|
|
365
|
+
r"""Return the fermionic mass spectrum :math:`m_f^2(\phi)` of the model.
|
|
366
|
+
|
|
367
|
+
The tuple is used in the fermionic one-loop correction
|
|
368
|
+
|
|
369
|
+
.. math::
|
|
370
|
+
V_1^{F}(\phi)
|
|
371
|
+
= -\sum_f \frac{n_f\,m_f^4(\phi)}{64\pi^2}
|
|
372
|
+
\left[\log\!\left(\frac{m_f^2(\phi)}{\mu^2}\right)-\tfrac{3}{2}\right].
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
X:
|
|
377
|
+
Field configuration :math:`\phi`.
|
|
378
|
+
"""
|
|
379
|
+
return self.mass_spectrum.fermion_massSq(X)
|
|
380
|
+
|
|
381
|
+
# Model initialisation checks -----------------------------------------------------
|
|
382
|
+
def checkInitialisation(self) -> None:
|
|
383
|
+
"""Check if all the necessary paramters in the subclass initialisation are set.
|
|
384
|
+
"""
|
|
385
|
+
if self.v_GeV is None:
|
|
386
|
+
raise errors.InitPotentialError(f"Need to specify v_GeV parameter!")
|
|
387
|
+
if self.daisy not in ["ArnoldEspinosa", "Parwani", "off"]:
|
|
388
|
+
raise errors.InitPotentialError(
|
|
389
|
+
f"Unknown daisy resummation method: {self.daisy}. Please choose 'ArnoldEspinosa', 'Parwani' or 'off'.")
|
|
390
|
+
if type(self.X0) == type(None):
|
|
391
|
+
raise errors.InitPotentialError(f"Global minimum X0 at zero temperature not specified!")
|
|
392
|
+
if self.renormScaleSq is None:
|
|
393
|
+
raise errors.InitPotentialError(f"Renormalisation scale renormScaleSq not specified!")
|
|
394
|
+
if not isinstance(self.mass_spectrum, MassSpectrum):
|
|
395
|
+
raise errors.InitPotentialError("Mass spectrum not configured. Set `self.mass_spectrum` during init().")
|
|
396
|
+
|
|
397
|
+
# Always do tachyon test!
|
|
398
|
+
self.tachyonTest(self.X0)
|
|
399
|
+
|
|
400
|
+
# EFFECTIVE POTENTIAL CALCULATIONS -----------------------
|
|
401
|
+
def tachyonTest(self, X0: np.ndarray) -> None:
|
|
402
|
+
"""
|
|
403
|
+
Check if the boson mass matrix has any tachyonic modes at the
|
|
404
|
+
zero-temperature minimum. This is used to check if the potential is
|
|
405
|
+
stable at zero temperature.
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
X0 : array_like
|
|
410
|
+
The zero-temperature minimum of the potential. This should be a
|
|
411
|
+
single point (with length `Ndim`).
|
|
412
|
+
|
|
413
|
+
Raises
|
|
414
|
+
------
|
|
415
|
+
TachyonError
|
|
416
|
+
If any of the boson masses are tachyonic.
|
|
417
|
+
"""
|
|
418
|
+
bosons0 = self.boson_massSq(X0, 0.0)
|
|
419
|
+
m20, _, _, is_physical = bosons0
|
|
420
|
+
|
|
421
|
+
# Check if any of the non-goldstone-boson scalars are tachyonic (m20 < 0)
|
|
422
|
+
is_tachyonic = np.logical_and(m20 < -1e-5, is_physical == True)
|
|
423
|
+
|
|
424
|
+
if is_tachyonic.any():
|
|
425
|
+
msg = "Tachyonic boson mass at zero temperature minimum, Mass matrix: " + str(m20/self.v_stable)
|
|
426
|
+
raise errors.TachyonError(msg)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def V0(self, X: np.ndarray) -> np.ndarray:
|
|
430
|
+
"""Tree level potential. This has to be overwritten by the
|
|
431
|
+
subclass.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
X : np.ndarray
|
|
436
|
+
Field values.
|
|
437
|
+
Returns
|
|
438
|
+
-------
|
|
439
|
+
np.ndarray
|
|
440
|
+
Potential values corresponding to the field values.
|
|
441
|
+
"""
|
|
442
|
+
return X * 0.0
|
|
443
|
+
|
|
444
|
+
def V1(self, bosons, fermions) -> np.ndarray:
|
|
445
|
+
"""
|
|
446
|
+
The one-loop corrections to the zero-temperature potential
|
|
447
|
+
using MS-bar renormalization.
|
|
448
|
+
|
|
449
|
+
This is generally not called directly, but is instead used by
|
|
450
|
+
:func:`Vtot`.
|
|
451
|
+
"""
|
|
452
|
+
m2, n, c, _ = bosons
|
|
453
|
+
y = np.sum(n * m2 * m2 * (np.log(np.abs(m2 / self.renormScaleSq) + 1e-100) - c), axis=-1)
|
|
454
|
+
|
|
455
|
+
m2, n = fermions
|
|
456
|
+
c = 3. / 2.
|
|
457
|
+
y -= np.sum(n * m2 * m2 * (np.log(np.abs(m2 / self.renormScaleSq) + 1e-100) - c), axis=-1)
|
|
458
|
+
|
|
459
|
+
return y / (64 * np.pi * np.pi)
|
|
460
|
+
|
|
461
|
+
def V1phys(self, bosons, fermions) -> np.ndarray:
|
|
462
|
+
"""
|
|
463
|
+
The one-loop corrections to the zero-temperature potential
|
|
464
|
+
using MS-bar renormalization, without the goldstone bosons.
|
|
465
|
+
"""
|
|
466
|
+
m2, n, c, phys = bosons
|
|
467
|
+
y = np.sum(n * m2 * m2 * phys * (np.log(np.abs(m2 / self.renormScaleSq) + 1e-100) - c), axis=-1)
|
|
468
|
+
|
|
469
|
+
m2 = fermions.masses_sq
|
|
470
|
+
if m2.size != 0:
|
|
471
|
+
n = fermions.dof
|
|
472
|
+
c = 3. / 2.
|
|
473
|
+
y -= np.sum(n * m2 * m2 * (np.log(np.abs(m2 / self.renormScaleSq) + 1e-100) - c), axis=-1)
|
|
474
|
+
|
|
475
|
+
return y / (64 * np.pi * np.pi)
|
|
476
|
+
|
|
477
|
+
def V1_from_X(self, X : np.ndarray) -> np.ndarray:
|
|
478
|
+
"""
|
|
479
|
+
The one-loop corrections to the zero-temperature potential
|
|
480
|
+
using MS-bar renormalization.
|
|
481
|
+
|
|
482
|
+
This is generally not called directly, but is instead used by
|
|
483
|
+
:func:`Vtot`.
|
|
484
|
+
"""
|
|
485
|
+
X = np.asanyarray(X, dtype=float)
|
|
486
|
+
bosons = self.boson_massSq(X, 0.0)
|
|
487
|
+
fermions = self.fermion_massSq(X)
|
|
488
|
+
return self.V1(bosons, fermions)
|
|
489
|
+
|
|
490
|
+
def Vct(self, X : np.ndarray) -> np.ndarray:
|
|
491
|
+
"""
|
|
492
|
+
The one-loop counterterm potential for MS-bar renormalization.
|
|
493
|
+
This should be overwritten.
|
|
494
|
+
"""
|
|
495
|
+
r = 0
|
|
496
|
+
return r
|
|
497
|
+
|
|
498
|
+
def check_renorm_conditions(
|
|
499
|
+
self,
|
|
500
|
+
X: np.ndarray | None = None,
|
|
501
|
+
*,
|
|
502
|
+
eps: float | None = None,
|
|
503
|
+
order: int | None = None,
|
|
504
|
+
) -> dict[str, np.ndarray]:
|
|
505
|
+
"""Return derivatives of V1 + Vct at the given field point.
|
|
506
|
+
|
|
507
|
+
This provides a quick diagnostic for OS-like counterterm conditions
|
|
508
|
+
that enforce vanishing first and second derivatives of V1 + Vct at the
|
|
509
|
+
reference vacuum.
|
|
510
|
+
"""
|
|
511
|
+
if X is None:
|
|
512
|
+
X = np.asarray(self.X0, dtype=float)
|
|
513
|
+
else:
|
|
514
|
+
X = np.asarray(X, dtype=float)
|
|
515
|
+
|
|
516
|
+
eps = self.x_eps if eps is None else eps
|
|
517
|
+
order = self.deriv_order if order is None else order
|
|
518
|
+
|
|
519
|
+
def V1_plus_ct(Xp):
|
|
520
|
+
bosons = self.boson_massSq(Xp, 0.0)
|
|
521
|
+
fermions = self.fermion_massSq(Xp)
|
|
522
|
+
return self.V1(bosons, fermions) + self.Vct(Xp)
|
|
523
|
+
|
|
524
|
+
grad_fn = helper_functions.gradientFunction(
|
|
525
|
+
V1_plus_ct, eps=eps, Ndim=self.Ndim, order=order
|
|
526
|
+
)
|
|
527
|
+
hess_fn = helper_functions.hessianFunction(
|
|
528
|
+
V1_plus_ct, eps=eps, Ndim=self.Ndim, order=order
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
grad = np.squeeze(grad_fn(X))
|
|
532
|
+
hess = np.squeeze(hess_fn(X))
|
|
533
|
+
grad_arr = np.atleast_1d(grad)
|
|
534
|
+
hess_arr = np.atleast_2d(hess)
|
|
535
|
+
offdiag = hess_arr - np.diag(np.diag(hess_arr))
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
"grad": grad_arr,
|
|
539
|
+
"hess": hess_arr,
|
|
540
|
+
"max_abs_grad": float(np.max(np.abs(grad_arr))) if grad_arr.size else 0.0,
|
|
541
|
+
"max_abs_hess": float(np.max(np.abs(hess_arr))) if hess_arr.size else 0.0,
|
|
542
|
+
"max_abs_offdiag": float(np.max(np.abs(offdiag))) if offdiag.size else 0.0,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
def V1T(self, bosons, fermions, T: float | np.ndarray) -> np.ndarray:
|
|
546
|
+
"""
|
|
547
|
+
The one-loop finite-temperature potential.
|
|
548
|
+
|
|
549
|
+
This is generally not called directly, but is instead used by
|
|
550
|
+
:func:`Vtot`.
|
|
551
|
+
|
|
552
|
+
Note
|
|
553
|
+
----
|
|
554
|
+
The `Jf` and `Jb` functions used here are
|
|
555
|
+
aliases for :func:`finiteT.Jf_spline` and :func:`finiteT.Jb_spline`,
|
|
556
|
+
each of which accept mass over temperature *squared* as inputs
|
|
557
|
+
(this allows for negative mass-squared values, which I take to be the
|
|
558
|
+
real part of the defining integrals.
|
|
559
|
+
|
|
560
|
+
"""
|
|
561
|
+
# This does not need to be overridden.
|
|
562
|
+
T2 = (T * T)[..., np.newaxis] + 1e-100
|
|
563
|
+
# the 1e-100 is to avoid divide by zero errors
|
|
564
|
+
T4 = T * T * T * T
|
|
565
|
+
msq, dof, _, _ = bosons
|
|
566
|
+
y = np.sum(dof * Jb(msq / T2), axis=-1)
|
|
567
|
+
msq, dof = fermions
|
|
568
|
+
y += np.sum(dof * Jf(msq / T2), axis=-1)
|
|
569
|
+
return y * T4 / (2 * np.pi * np.pi)
|
|
570
|
+
|
|
571
|
+
def V1T_from_X(self, X: np.ndarray, T: float | np.ndarray) -> np.ndarray:
|
|
572
|
+
"""
|
|
573
|
+
Calculates the mass matrix and resulting one-loop finite-T potential.
|
|
574
|
+
|
|
575
|
+
Useful when calculate temperature derivatives, when the zero-temperature
|
|
576
|
+
contributions don't matter.
|
|
577
|
+
"""
|
|
578
|
+
T = np.asanyarray(T, dtype=float)
|
|
579
|
+
X = np.asanyarray(X, dtype=float)
|
|
580
|
+
fermions = self.fermion_massSq(X)
|
|
581
|
+
if self.daisy == "off":
|
|
582
|
+
bosons0 = self.boson_massSq(X, 0)
|
|
583
|
+
return self.V1T(bosons0, fermions, T)
|
|
584
|
+
elif self.daisy == "Parwani":
|
|
585
|
+
# Parwani (1992) prescription. All modes resummed.
|
|
586
|
+
bosonsT = self.boson_massSq(X, T)
|
|
587
|
+
return self.V1T(bosonsT, fermions, T)
|
|
588
|
+
elif self.daisy == "ArnoldEspinosa":
|
|
589
|
+
# Carrington (1992), Arnold and Espinosa (1992) prescription. Zero modes only.
|
|
590
|
+
bosons0 = self.boson_massSq(X, 0)
|
|
591
|
+
return self.V1T(bosons0, fermions, T)
|
|
592
|
+
|
|
593
|
+
def constantTerms(self, T: float | np.ndarray, include_decoupled=False) -> np.ndarray:
|
|
594
|
+
r"""Add the field-independent, but temperature-dependent terms to the
|
|
595
|
+
effective potential, i.e. radiation. This allows the calculation
|
|
596
|
+
of the energy density as
|
|
597
|
+
|
|
598
|
+
.. math::
|
|
599
|
+
\rho = V - \partial_T V
|
|
600
|
+
|
|
601
|
+
This should be computed from the particle definitions in the
|
|
602
|
+
potential.
|
|
603
|
+
|
|
604
|
+
Parameters
|
|
605
|
+
----------
|
|
606
|
+
X : array_like
|
|
607
|
+
The field values
|
|
608
|
+
T : float or array_like
|
|
609
|
+
The temperatures
|
|
610
|
+
include_decoupled : bool, optional
|
|
611
|
+
If False, don't include the contribution of the specified decoupled
|
|
612
|
+
radiation bath specified in `self.kin_decoupled_p_geff`.
|
|
613
|
+
|
|
614
|
+
Returns
|
|
615
|
+
-------
|
|
616
|
+
array_like
|
|
617
|
+
The field-independent but temperature-dependent contribution to
|
|
618
|
+
the effective potential.
|
|
619
|
+
"""
|
|
620
|
+
T = np.asanyarray(T, dtype=float)
|
|
621
|
+
geff = self.kin_coupled_p_geff(T, self.conversionFactor)
|
|
622
|
+
|
|
623
|
+
# in the landau gauge we have -1 massless DOF from each gauge boson
|
|
624
|
+
# (rather from the ghost fields)
|
|
625
|
+
geff -= self.mass_spectrum.number_gauge_bosons
|
|
626
|
+
|
|
627
|
+
if include_decoupled:
|
|
628
|
+
geff += self.kin_decoupled_p_geff(T, self.conversionFactor)
|
|
629
|
+
return -np.pi**2/90 * geff * T**4
|
|
630
|
+
|
|
631
|
+
def Vtot(self, X: np.ndarray, T: float | np.ndarray, include_radiation=True,
|
|
632
|
+
include_decoupled=True) -> np.ndarray:
|
|
633
|
+
"""
|
|
634
|
+
The total finite temperature effective potential is calculated by adding
|
|
635
|
+
up the tree level potential, the one-loop-zero-T correction, the respective
|
|
636
|
+
counter terms, and (depending on the daisy resummation scheme) the one-loop-
|
|
637
|
+
temperature-dependent corrections.
|
|
638
|
+
|
|
639
|
+
Parameters
|
|
640
|
+
----------
|
|
641
|
+
X : array_like
|
|
642
|
+
Field value(s).
|
|
643
|
+
Either a single point (with length `Ndim`), or an array of points.
|
|
644
|
+
T : float or array_like
|
|
645
|
+
The temperature. The shapes of `X` and `T`
|
|
646
|
+
should be such that ``X.shape[:-1]`` and ``T.shape`` are
|
|
647
|
+
broadcastable (that is, ``X[...,0]*T`` is a valid operation).
|
|
648
|
+
include_radiation : bool, optional
|
|
649
|
+
If False, this will drop all field-independent radiation
|
|
650
|
+
terms from the effective potential. Useful for calculating
|
|
651
|
+
differences or derivatives.
|
|
652
|
+
include_decoupled : bool, optional
|
|
653
|
+
If True, include radiation terms of the specified decoupled
|
|
654
|
+
radiation bath, if false, don't include them. This is important,
|
|
655
|
+
for e.g. the calculation of alpha for a decoupled SM and DS.
|
|
656
|
+
|
|
657
|
+
Returns
|
|
658
|
+
----------
|
|
659
|
+
Vtot : array_like
|
|
660
|
+
Total effective potential.
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
T = np.asanyarray(T)
|
|
664
|
+
X = np.asanyarray(X)
|
|
665
|
+
|
|
666
|
+
bosons0 = self.boson_massSq(X, T*0.0)
|
|
667
|
+
bosonsT = self.boson_massSq(X, T)
|
|
668
|
+
fermions = self.fermion_massSq(X)
|
|
669
|
+
|
|
670
|
+
y = self.V0(X) # Tree-level
|
|
671
|
+
y += self.V1(bosons0, fermions) # One-loop zero-T correction
|
|
672
|
+
y += self.Vct(X) # Counter-term potential
|
|
673
|
+
|
|
674
|
+
if self.daisy == "off":
|
|
675
|
+
y += self.V1T(bosons0, fermions, T)
|
|
676
|
+
elif self.daisy == "Parwani":
|
|
677
|
+
# Parwani (1992) prescription. All modes resummed.
|
|
678
|
+
y += self.V1T(bosonsT, fermions, T)
|
|
679
|
+
elif self.daisy == "ArnoldEspinosa":
|
|
680
|
+
# Carrington (1992), Arnold and Espinosa (1992) prescription. Zero modes only.
|
|
681
|
+
Vdaisy = self.Vdaisy(bosons0, bosonsT, T)
|
|
682
|
+
y += self.V1T(bosons0, fermions, T) + Vdaisy
|
|
683
|
+
|
|
684
|
+
# Add field-independent terms, so that the energy density can be computed
|
|
685
|
+
# correctly
|
|
686
|
+
if include_radiation:
|
|
687
|
+
y += self.constantTerms(T, include_decoupled=include_decoupled)
|
|
688
|
+
|
|
689
|
+
return y
|
|
690
|
+
|
|
691
|
+
def Vdaisy(self, bosons0, bosonsT , T: float | np.ndarray
|
|
692
|
+
) -> float | np.ndarray:
|
|
693
|
+
"""
|
|
694
|
+
Calculate the daisy resummation term for the Arnold-Espinosa
|
|
695
|
+
prescription.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
bosons0 (tuple): Boson mass spectrum at zero temperature.
|
|
699
|
+
bosonsT (tuple): Boson mass spectrum at finite temperature.
|
|
700
|
+
T (float): Temperature.
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
float: Daisy resummation potential correction to V1T.
|
|
704
|
+
"""
|
|
705
|
+
m20, nb, _, _ = bosons0
|
|
706
|
+
m2T, _, _, _ = bosonsT
|
|
707
|
+
|
|
708
|
+
y = np.real(-(T/(12.*np.pi))*np.sum(nb * (pow(m2T+0j, 1.5) - pow(m20+0j, 1.5)), axis=-1))
|
|
709
|
+
return y
|
|
710
|
+
|
|
711
|
+
def Vdaisy_from_X(self, X, T: float | np.ndarray,
|
|
712
|
+
realDaisy=False) -> float | np.ndarray:
|
|
713
|
+
"""
|
|
714
|
+
Calculate the daisy resummation term for the Arnold-Espinosa
|
|
715
|
+
prescription.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
bosons0 (tuple): Boson mass spectrum at zero temperature.
|
|
719
|
+
bosonsT (tuple): Boson mass spectrum at finite temperature.
|
|
720
|
+
T (float): Temperature.
|
|
721
|
+
realDaisy (bool): If True, enforce real-valued calculations.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
float: Daisy resummation potential correction to V1T.
|
|
725
|
+
"""
|
|
726
|
+
T = np.asanyarray(T, dtype=float)
|
|
727
|
+
X = np.asanyarray(X, dtype=float)
|
|
728
|
+
bosons0 = self.boson_massSq(X, 0)
|
|
729
|
+
bosons = self.boson_massSq(X, T)
|
|
730
|
+
return self.Vdaisy(bosons0, bosons, T)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def DVtot(self, X: np.ndarray, T: float | np.ndarray) -> np.ndarray:
|
|
734
|
+
"""
|
|
735
|
+
The finite temperature effective potential, but offset
|
|
736
|
+
such that V(0, T) = 0.
|
|
737
|
+
|
|
738
|
+
Parameters
|
|
739
|
+
----------
|
|
740
|
+
X : array_like
|
|
741
|
+
Field value(s).
|
|
742
|
+
Either a single point (with length `Ndim`), or an array of points.
|
|
743
|
+
T : float or array_like
|
|
744
|
+
The temperature. The shapes of `X` and `T`
|
|
745
|
+
should be such that ``X.shape[:-1]`` and ``T.shape`` are
|
|
746
|
+
broadcastable (that is, ``X[...,0]*T`` is a valid operation).
|
|
747
|
+
|
|
748
|
+
Returns
|
|
749
|
+
-------
|
|
750
|
+
np.ndarray
|
|
751
|
+
The effective potential at the given field values and temperatures,
|
|
752
|
+
offset such that V(0, T) = 0.
|
|
753
|
+
"""
|
|
754
|
+
X = np.array(X)
|
|
755
|
+
return self.Vtot(X, T, False) - self.Vtot(0 * X, T, False)
|
|
756
|
+
|
|
757
|
+
def gradV(self, X: np.ndarray, T: float | np.ndarray) -> np.ndarray:
|
|
758
|
+
"""
|
|
759
|
+
Find the gradient of the full effective potential.
|
|
760
|
+
|
|
761
|
+
This uses :func:`helper_functions.gradientFunction` to calculate the
|
|
762
|
+
gradient using finite differences, with differences
|
|
763
|
+
given by `self.x_eps`. Note that `self.x_eps` is only used directly
|
|
764
|
+
the first time this function is called, so subsequently changing it
|
|
765
|
+
will not have an effect.
|
|
766
|
+
|
|
767
|
+
Parameters
|
|
768
|
+
----------
|
|
769
|
+
X : array_like
|
|
770
|
+
Field value(s).
|
|
771
|
+
Either a single point (with length `Ndim`), or an array of points.
|
|
772
|
+
T : float or array_like
|
|
773
|
+
The temperature. The shapes of `X` and `T`
|
|
774
|
+
should be such that ``X.shape[:-1]`` and ``T.shape`` are
|
|
775
|
+
broadcastable (that is, ``X[...,0]*T`` is a valid operation).
|
|
776
|
+
|
|
777
|
+
Returns
|
|
778
|
+
-------
|
|
779
|
+
np.ndarray
|
|
780
|
+
The gradient of the effective potential at the given field values
|
|
781
|
+
and temperatures.
|
|
782
|
+
"""
|
|
783
|
+
try:
|
|
784
|
+
f = self._gradV
|
|
785
|
+
except BaseException:
|
|
786
|
+
# Create the gradient function
|
|
787
|
+
self._gradV = helper_functions.gradientFunction(
|
|
788
|
+
self.Vtot, self.x_eps, self.Ndim, self.deriv_order)
|
|
789
|
+
f = self._gradV
|
|
790
|
+
# Need to add extra axes to T since extra axes get added to X in
|
|
791
|
+
# the helper function.
|
|
792
|
+
T = np.asanyarray(T)[..., np.newaxis, np.newaxis]
|
|
793
|
+
return f(X, T, False)
|
|
794
|
+
|
|
795
|
+
def dV1atvev(self):
|
|
796
|
+
"""
|
|
797
|
+
Find the first derivative of the one loop zero temperature potential,
|
|
798
|
+
evaluated at the vev. Used in the function Vct to calculate the
|
|
799
|
+
counter term lagrangian
|
|
800
|
+
|
|
801
|
+
This uses :func:`helper_functions.gradientFunction` to calculate the
|
|
802
|
+
gradient using finite differences, with differences
|
|
803
|
+
given by `self.x_eps`. Note that `self.x_eps` is only used directly
|
|
804
|
+
the first time this function is called, so subsequently changing it
|
|
805
|
+
will not have an effect.
|
|
806
|
+
|
|
807
|
+
Returns
|
|
808
|
+
-------
|
|
809
|
+
np.ndarray
|
|
810
|
+
The gradient of the one-loop zero-temperature potential at the vev.
|
|
811
|
+
|
|
812
|
+
.. todo::
|
|
813
|
+
Currently, this function assumes that the vev is a point in
|
|
814
|
+
one-dimensional field space. Generalize to arbitrary number of
|
|
815
|
+
dimensions.
|
|
816
|
+
"""
|
|
817
|
+
|
|
818
|
+
try:
|
|
819
|
+
res = self._dV1atvev
|
|
820
|
+
|
|
821
|
+
except BaseException:
|
|
822
|
+
# vev means here an arbitrary point in field space
|
|
823
|
+
def V_coleman_weinberg(X):
|
|
824
|
+
bosonsvev0 = self.boson_massSq(X, 0.0)
|
|
825
|
+
fermionsvev = self.fermion_massSq(X)
|
|
826
|
+
return self.V1(bosonsvev0, fermionsvev)
|
|
827
|
+
|
|
828
|
+
dV1 = helper_functions.gradientFunction(
|
|
829
|
+
V_coleman_weinberg, eps=self.x_eps, Ndim=self.Ndim, order=self.deriv_order)
|
|
830
|
+
|
|
831
|
+
# Use np.squeeze to shrink array sizes from (1,1) and (1,) to a 0d
|
|
832
|
+
# array... otherways problems when evaluating Vtot at more than one
|
|
833
|
+
# point in field space at the same time in the code.
|
|
834
|
+
|
|
835
|
+
self._dV1atvev = np.squeeze(dV1(np.array([self.v])))
|
|
836
|
+
res = self._dV1atvev
|
|
837
|
+
return res
|
|
838
|
+
|
|
839
|
+
def dV1physatvev(self):
|
|
840
|
+
"""
|
|
841
|
+
Find the first derivative of the one loop zero temperature potential without goldstone bosons,
|
|
842
|
+
evaluated at the vev. Used in the function Vct to calculate the
|
|
843
|
+
counter term lagrangian
|
|
844
|
+
|
|
845
|
+
This uses :func:`helper_functions.gradientFunction` to calculate the
|
|
846
|
+
gradient using finite differences, with differences
|
|
847
|
+
given by `self.x_eps`. Note that `self.x_eps` is only used directly
|
|
848
|
+
the first time this function is called, so subsequently changing it
|
|
849
|
+
will not have an effect.
|
|
850
|
+
|
|
851
|
+
Returns
|
|
852
|
+
-------
|
|
853
|
+
np.ndarray
|
|
854
|
+
The gradient of the one-loop zero-temperature potential without
|
|
855
|
+
goldstone bosons at the vev.
|
|
856
|
+
|
|
857
|
+
.. todo::
|
|
858
|
+
Currently, this function assumes that the vev is a point in
|
|
859
|
+
one-dimensional field space. Generalize to arbitrary number of
|
|
860
|
+
dimensions.
|
|
861
|
+
"""
|
|
862
|
+
|
|
863
|
+
try:
|
|
864
|
+
res = self._dV1physatvev
|
|
865
|
+
except BaseException:
|
|
866
|
+
# vev means here an arbitrary point in field space
|
|
867
|
+
def V_coleman_weinberg(X):
|
|
868
|
+
bosonsvev0 = self.boson_massSq(X, 0.0)
|
|
869
|
+
fermionsvev = self.fermion_massSq(X)
|
|
870
|
+
return self.V1phys(bosonsvev0, fermionsvev)
|
|
871
|
+
dV1 = helper_functions.gradientFunction(
|
|
872
|
+
V_coleman_weinberg, eps = self.x_eps, Ndim = self.Ndim, order = self.deriv_order)
|
|
873
|
+
|
|
874
|
+
# Use np.squeeze to shrink array sizes from (1,1) and (1,) to a 0d
|
|
875
|
+
# array... otherways problems when evaluating Vtot at more than one
|
|
876
|
+
# point in field space at the same time in the code.
|
|
877
|
+
|
|
878
|
+
self._dV1physatvev = np.squeeze(dV1(np.array([self.v])))
|
|
879
|
+
res = self._dV1physatvev
|
|
880
|
+
return res
|
|
881
|
+
|
|
882
|
+
def dgradV_dT(self, X, T):
|
|
883
|
+
"""
|
|
884
|
+
Find the derivative of the gradient with respect to temperature.
|
|
885
|
+
|
|
886
|
+
This is useful when trying to follow the minima of the potential as they
|
|
887
|
+
move with temperature.
|
|
888
|
+
|
|
889
|
+
This uses :func:`helper_functions.gradientFunction` to calculate the
|
|
890
|
+
gradient using finite differences, with differences
|
|
891
|
+
given by `self.x_eps`. Note that `self.x_eps` is only used directly
|
|
892
|
+
the first time this function is called, so subsequently changing it
|
|
893
|
+
will not have an effect.
|
|
894
|
+
|
|
895
|
+
Parameters
|
|
896
|
+
----------
|
|
897
|
+
X : array_like
|
|
898
|
+
Field value(s).
|
|
899
|
+
Either a single point (with length `Ndim`), or an array of points.
|
|
900
|
+
T : float or array_like
|
|
901
|
+
The temperature. The shapes of `X` and `T`
|
|
902
|
+
should be such that ``X.shape[:-1]`` and ``T.shape`` are
|
|
903
|
+
broadcastable (that is, ``X[...,0]*T`` is a valid operation).
|
|
904
|
+
|
|
905
|
+
Returns
|
|
906
|
+
-------
|
|
907
|
+
np.ndarray
|
|
908
|
+
The derivative of the gradient of the effective potential with
|
|
909
|
+
respect to temperature at the given field values and temperatures.
|
|
910
|
+
"""
|
|
911
|
+
T_eps = self.T_eps
|
|
912
|
+
try:
|
|
913
|
+
gradVT = self._gradVT
|
|
914
|
+
except BaseException:
|
|
915
|
+
# Create the gradient function
|
|
916
|
+
self._gradVT = helper_functions.gradientFunction(
|
|
917
|
+
self.V1T_from_X, self.x_eps, self.Ndim, self.deriv_order)
|
|
918
|
+
gradVT = self._gradVT
|
|
919
|
+
# Need to add extra axes to T since extra axes get added to X in
|
|
920
|
+
# the helper function.
|
|
921
|
+
T = np.asanyarray(T)[..., np.newaxis, np.newaxis]
|
|
922
|
+
assert (self.deriv_order == 2 or self.deriv_order == 4)
|
|
923
|
+
if self.deriv_order == 2:
|
|
924
|
+
y = gradVT(X, T + T_eps) - gradVT(X, T - T_eps)
|
|
925
|
+
y *= 1. / (2 * T_eps)
|
|
926
|
+
else:
|
|
927
|
+
y = gradVT(X, T - 2 * T_eps)
|
|
928
|
+
y -= 8 * gradVT(X, T - T_eps)
|
|
929
|
+
y += 8 * gradVT(X, T + T_eps)
|
|
930
|
+
y -= gradVT(X, T + 2 * T_eps)
|
|
931
|
+
y *= 1. / (12 * T_eps)
|
|
932
|
+
return y
|
|
933
|
+
|
|
934
|
+
def dVdT(self, X : np.ndarray, T: float, dT: float, include_radiation=True,
|
|
935
|
+
include_decoupled=True):
|
|
936
|
+
"""Find the derivative of the potential with respect to the temperature.
|
|
937
|
+
|
|
938
|
+
Parameters
|
|
939
|
+
----------
|
|
940
|
+
X : np.ndarray
|
|
941
|
+
Field value(s).
|
|
942
|
+
Either a single point (with length `Ndim`), or an array of points.
|
|
943
|
+
T : float
|
|
944
|
+
The temperature.
|
|
945
|
+
dT : float
|
|
946
|
+
The change in temperature.
|
|
947
|
+
include_radiation : bool
|
|
948
|
+
Whether to include radiation contributions.
|
|
949
|
+
include_decoupled : bool
|
|
950
|
+
Whether to include decoupled-radiation contributions.
|
|
951
|
+
|
|
952
|
+
Returns
|
|
953
|
+
-------
|
|
954
|
+
np.ndarray
|
|
955
|
+
The derivative of the potential with respect to temperature at the given
|
|
956
|
+
field values and temperatures.
|
|
957
|
+
"""
|
|
958
|
+
dV = (
|
|
959
|
+
self.Vtot(X, T - 2*dT, include_radiation=include_radiation,
|
|
960
|
+
include_decoupled=include_decoupled)
|
|
961
|
+
- 8*self.Vtot(X, T - dT, include_radiation=include_radiation,
|
|
962
|
+
include_decoupled=include_decoupled)
|
|
963
|
+
+ 8*self.Vtot(X, T + dT, include_radiation=include_radiation,
|
|
964
|
+
include_decoupled=include_decoupled)
|
|
965
|
+
- self.Vtot(X, T + 2*dT, include_radiation=include_radiation,
|
|
966
|
+
include_decoupled=include_decoupled)
|
|
967
|
+
)
|
|
968
|
+
dV = dV / (12 * dT)
|
|
969
|
+
return dV
|
|
970
|
+
|
|
971
|
+
def d2VdT2(self, X : np.ndarray, T: float, dT: float | None = None,
|
|
972
|
+
include_radiation=True, include_decoupled=True) -> np.ndarray:
|
|
973
|
+
"""Find the second derivative of the potential with respect to the temperature.
|
|
974
|
+
|
|
975
|
+
Parameters
|
|
976
|
+
----------
|
|
977
|
+
X : np.ndarray
|
|
978
|
+
Field value(s).
|
|
979
|
+
Either a single point (with length `Ndim`), or an array of points.
|
|
980
|
+
T : float
|
|
981
|
+
The temperature.
|
|
982
|
+
dT : float, optional
|
|
983
|
+
Temperature step for the finite-difference stencil. Defaults to
|
|
984
|
+
the tracing temperature accuracy.
|
|
985
|
+
include_radiation : bool
|
|
986
|
+
Whether to include radiation contributions.
|
|
987
|
+
include_decoupled : bool
|
|
988
|
+
Whether to include decoupled-radiation contributions.
|
|
989
|
+
|
|
990
|
+
Returns
|
|
991
|
+
-------
|
|
992
|
+
np.ndarray
|
|
993
|
+
The second derivative of the potential with respect to temperature at the given
|
|
994
|
+
field values and temperatures.
|
|
995
|
+
"""
|
|
996
|
+
if dT is None:
|
|
997
|
+
dT = self.T_eps
|
|
998
|
+
ddV = (
|
|
999
|
+
-self.Vtot(X, T - 2*dT, include_radiation=include_radiation,
|
|
1000
|
+
include_decoupled=include_decoupled)
|
|
1001
|
+
+ 16*self.Vtot(X, T - dT, include_radiation=include_radiation,
|
|
1002
|
+
include_decoupled=include_decoupled)
|
|
1003
|
+
- 30*self.Vtot(X, T, include_radiation=include_radiation,
|
|
1004
|
+
include_decoupled=include_decoupled)
|
|
1005
|
+
+ 16*self.Vtot(X, T + dT, include_radiation=include_radiation,
|
|
1006
|
+
include_decoupled=include_decoupled)
|
|
1007
|
+
- self.Vtot(X, T + 2*dT, include_radiation=include_radiation,
|
|
1008
|
+
include_decoupled=include_decoupled)
|
|
1009
|
+
) / (12*dT**2)
|
|
1010
|
+
return ddV
|
|
1011
|
+
|
|
1012
|
+
def massSqMatrix(self, X):
|
|
1013
|
+
"""
|
|
1014
|
+
Calculate the tree-level mass square matrix of the scalar field.
|
|
1015
|
+
|
|
1016
|
+
This uses :func:`helper_functions.hessianFunction` to calculate the
|
|
1017
|
+
matrix using finite differences, with differences
|
|
1018
|
+
given by `self.x_eps`. Note that `self.x_eps` is only used directly
|
|
1019
|
+
the first time this function is called, so subsequently changing it
|
|
1020
|
+
will not have an effect.
|
|
1021
|
+
|
|
1022
|
+
The resulting matrix will have rank `Ndim`. This function may be useful
|
|
1023
|
+
for subclasses in finding the boson particle spectrum.
|
|
1024
|
+
|
|
1025
|
+
Parameters
|
|
1026
|
+
----------
|
|
1027
|
+
X : array_like
|
|
1028
|
+
Field value(s).
|
|
1029
|
+
Either a single point (with length `Ndim`), or an array of points.
|
|
1030
|
+
|
|
1031
|
+
Returns
|
|
1032
|
+
-------
|
|
1033
|
+
np.ndarray
|
|
1034
|
+
The tree-level mass square matrix at the given field values.
|
|
1035
|
+
"""
|
|
1036
|
+
try:
|
|
1037
|
+
f = self._massSqMatrix
|
|
1038
|
+
except BaseException:
|
|
1039
|
+
# Create the gradient function
|
|
1040
|
+
self._massSqMatrix = helper_functions.hessianFunction(
|
|
1041
|
+
self.V0, self.x_eps, self.Ndim, self.deriv_order)
|
|
1042
|
+
f = self._massSqMatrix
|
|
1043
|
+
return f(X)
|
|
1044
|
+
|
|
1045
|
+
def d2V(self, X, T):
|
|
1046
|
+
"""
|
|
1047
|
+
Calculates the Hessian (second derivative) matrix for the
|
|
1048
|
+
finite-temperature effective potential.
|
|
1049
|
+
|
|
1050
|
+
This uses :func:`helper_functions.hessianFunction` to calculate the
|
|
1051
|
+
matrix using finite differences, with differences
|
|
1052
|
+
given by `self.x_eps`. Note that `self.x_eps` is only used directly
|
|
1053
|
+
the first time this function is called, so subsequently changing it
|
|
1054
|
+
will not have an effect.
|
|
1055
|
+
|
|
1056
|
+
Parameters
|
|
1057
|
+
----------
|
|
1058
|
+
X : array_like
|
|
1059
|
+
Field value(s).
|
|
1060
|
+
Either a single point (with length `Ndim`), or an array of points.
|
|
1061
|
+
T : float or array_like
|
|
1062
|
+
The temperature. The shapes of `X` and `T`
|
|
1063
|
+
should be such that ``X.shape[:-1]`` and ``T.shape`` are
|
|
1064
|
+
broadcastable (that is, ``X[...,0]*T`` is a valid operation).
|
|
1065
|
+
|
|
1066
|
+
Returns
|
|
1067
|
+
-------
|
|
1068
|
+
np.ndarray
|
|
1069
|
+
The Hessian matrix of the effective potential at the given field
|
|
1070
|
+
values and temperatures.
|
|
1071
|
+
"""
|
|
1072
|
+
try:
|
|
1073
|
+
f = self._d2V
|
|
1074
|
+
except BaseException:
|
|
1075
|
+
# Create the gradient function
|
|
1076
|
+
self._d2V = helper_functions.hessianFunction(
|
|
1077
|
+
self.Vtot, self.x_eps, self.Ndim, self.deriv_order)
|
|
1078
|
+
f = self._d2V
|
|
1079
|
+
# Need to add extra axes to T since extra axes get added to X in
|
|
1080
|
+
# the helper function.
|
|
1081
|
+
T = np.asanyarray(T)[..., np.newaxis]
|
|
1082
|
+
return f(X, T, False)
|
|
1083
|
+
|
|
1084
|
+
def d2V1atvev(self):
|
|
1085
|
+
"""
|
|
1086
|
+
Find the second derivative of the one loop zero temperature potential,
|
|
1087
|
+
evaluated at the vev. Used in the function Vct to calculate the
|
|
1088
|
+
counter term lagrangian
|
|
1089
|
+
|
|
1090
|
+
This uses :func:`helper_functions.gradientFunction` to calculate the
|
|
1091
|
+
gradient using finite differences, with differences
|
|
1092
|
+
given by `self.x_eps`. Note that `self.x_eps` is only used directly
|
|
1093
|
+
the first time this function is called, so subsequently changing it
|
|
1094
|
+
will not have an effect.
|
|
1095
|
+
|
|
1096
|
+
Returns
|
|
1097
|
+
-------
|
|
1098
|
+
np.ndarray
|
|
1099
|
+
The Hessian of the one-loop zero-temperature potential at the vev.
|
|
1100
|
+
|
|
1101
|
+
.. todo::
|
|
1102
|
+
Currently, this function assumes that the vev is a point in
|
|
1103
|
+
one-dimensional field space. Generalize to arbitrary number of
|
|
1104
|
+
dimensions.
|
|
1105
|
+
"""
|
|
1106
|
+
|
|
1107
|
+
try:
|
|
1108
|
+
res = self._d2V1atvev
|
|
1109
|
+
except BaseException:
|
|
1110
|
+
# vev means here an arbitrary point in field space
|
|
1111
|
+
def V_coleman_weinberg(X):
|
|
1112
|
+
bosonsvev0 = self.boson_massSq(X, 0.0)
|
|
1113
|
+
fermionsvev = self.fermion_massSq(X)
|
|
1114
|
+
return self.V1(bosonsvev0, fermionsvev)
|
|
1115
|
+
|
|
1116
|
+
d2V1 = helper_functions.hessianFunction(
|
|
1117
|
+
V_coleman_weinberg, eps=self.x_eps, Ndim=self.Ndim, order=self.deriv_order)
|
|
1118
|
+
|
|
1119
|
+
# Use np.squeeze to shrink array sizes from (1,1) and (1,) to a 0d
|
|
1120
|
+
# array... otherways problems when evaluating Vtot at more than one
|
|
1121
|
+
# point in field space at the same time in the code.
|
|
1122
|
+
self._d2V1atvev = np.squeeze(d2V1(np.array([self.v])))
|
|
1123
|
+
res = self._d2V1atvev
|
|
1124
|
+
return res
|
|
1125
|
+
|
|
1126
|
+
# MINIMIZATION AND TRANSITION ANALYSIS --------------------------------
|
|
1127
|
+
|
|
1128
|
+
def approxZeroTMin(self):
|
|
1129
|
+
"""
|
|
1130
|
+
Returns approximate values of the zero-temperature minima.
|
|
1131
|
+
|
|
1132
|
+
This should be overridden by subclasses, although it is not strictly
|
|
1133
|
+
necessary if there is only one minimum at tree level. The precise values
|
|
1134
|
+
of the minima will later be found using :func:`scipy.optimize.fmin`.
|
|
1135
|
+
|
|
1136
|
+
Returns
|
|
1137
|
+
-------
|
|
1138
|
+
minima : list
|
|
1139
|
+
A list of points of the approximate minima.
|
|
1140
|
+
"""
|
|
1141
|
+
# This should be overridden.
|
|
1142
|
+
return [np.ones(self.Ndim) * self.renormScaleSq**.5]
|
|
1143
|
+
|
|
1144
|
+
def findMinimum(self, X : np.ndarray=None, T : float=0.0) -> np.ndarray:
|
|
1145
|
+
"""
|
|
1146
|
+
Convenience function for finding the nearest minimum to `X` at
|
|
1147
|
+
temperature `T`.
|
|
1148
|
+
|
|
1149
|
+
Parameters
|
|
1150
|
+
----------
|
|
1151
|
+
X : np.ndarray, optional
|
|
1152
|
+
Starting point for the minimization. If None, use the
|
|
1153
|
+
approximate zero-temperature minimum.
|
|
1154
|
+
T : float, optional
|
|
1155
|
+
Temperature at which to find the minimum. Default is 0.0.
|
|
1156
|
+
|
|
1157
|
+
Returns
|
|
1158
|
+
-------
|
|
1159
|
+
np.ndarray
|
|
1160
|
+
The location of the minimum found.
|
|
1161
|
+
"""
|
|
1162
|
+
if X is None:
|
|
1163
|
+
X = self.approxZeroTMin()[0]
|
|
1164
|
+
return optimize.fmin(self.Vtot, X, args=(T,), disp=0)
|
|
1165
|
+
|
|
1166
|
+
def generateInvGroupElements(self) -> None:
|
|
1167
|
+
r"""Generate the group elements under which the potential
|
|
1168
|
+
is invariant. The group is
|
|
1169
|
+
|
|
1170
|
+
.. math::
|
|
1171
|
+
G = \Pi_i^N \mathbb{Z}_2
|
|
1172
|
+
|
|
1173
|
+
representing the discrete symmetries of the potential with scalar field
|
|
1174
|
+
dimension N.
|
|
1175
|
+
|
|
1176
|
+
.. todo:: test, refactor and clean up
|
|
1177
|
+
|
|
1178
|
+
Parameters
|
|
1179
|
+
----------
|
|
1180
|
+
|
|
1181
|
+
Returns
|
|
1182
|
+
-------
|
|
1183
|
+
None."""
|
|
1184
|
+
# First generate the elements of the nplet of C2
|
|
1185
|
+
groupElements = [np.identity(self.Ndim)]
|
|
1186
|
+
for i in range(self.Ndim):
|
|
1187
|
+
oldGroupElements = groupElements.copy()
|
|
1188
|
+
for g in oldGroupElements:
|
|
1189
|
+
gnew = g.copy()
|
|
1190
|
+
gnew[i, i] = -1
|
|
1191
|
+
groupElements.append(gnew)
|
|
1192
|
+
|
|
1193
|
+
# Now check under which of these the potential is invariant
|
|
1194
|
+
# For this we take a random point at positive field values,
|
|
1195
|
+
# apply the transformations \phi -> g \phi with g \element G and
|
|
1196
|
+
# check if Vtree(\phi) = Vtree(g \phi)
|
|
1197
|
+
|
|
1198
|
+
# For this we check 5 points
|
|
1199
|
+
Npoints = 5
|
|
1200
|
+
transformedThreshold = 1e-5 # relative difference between the pot values
|
|
1201
|
+
randPoints = (0.01 + np.random.rand(Npoints, self.Ndim)*0.49) * self.v
|
|
1202
|
+
randT = self.Tmin + (np.random.rand(Npoints) * (self.Tmax - self.Tmin))
|
|
1203
|
+
|
|
1204
|
+
invGroupElements = []
|
|
1205
|
+
for i in range(len(groupElements)):
|
|
1206
|
+
g = groupElements.pop()
|
|
1207
|
+
V = self.Vtot(randPoints, randT) + 1e-20
|
|
1208
|
+
Vbar = self.Vtot(randPoints @ g, randT) + 1e-20
|
|
1209
|
+
if (np.abs((V - Vbar)/V) < transformedThreshold).all():
|
|
1210
|
+
invGroupElements.append(g)
|
|
1211
|
+
|
|
1212
|
+
self.invGroupElements = invGroupElements
|
|
1213
|
+
|
|
1214
|
+
def applySymmetries(self, X: np.ndarray) -> np.ndarray:
|
|
1215
|
+
r"""Check if the point is in the right quadrant of field space
|
|
1216
|
+
and transform it if necessary.
|
|
1217
|
+
|
|
1218
|
+
This is done in the fashion of BSMPT with the measure
|
|
1219
|
+
|
|
1220
|
+
..math::
|
|
1221
|
+
M(g \phi) = \sum_i 2^i \theta((g \phi)_i)
|
|
1222
|
+
|
|
1223
|
+
This measure is maximised for one unique 'quadrant' in fieldspace.
|
|
1224
|
+
|
|
1225
|
+
Parameters
|
|
1226
|
+
----------
|
|
1227
|
+
X : np.ndarray
|
|
1228
|
+
Point in field space.
|
|
1229
|
+
|
|
1230
|
+
Returns
|
|
1231
|
+
-------
|
|
1232
|
+
np.ndarray
|
|
1233
|
+
The transformed point."""
|
|
1234
|
+
maxMeasure = 2**self.Ndim - 1
|
|
1235
|
+
|
|
1236
|
+
measure = -1 # Store the largest measure
|
|
1237
|
+
Xmax = X.copy()
|
|
1238
|
+
for g in self.invGroupElements:
|
|
1239
|
+
newMeasure = 0
|
|
1240
|
+
XX = X @ g
|
|
1241
|
+
for i in range(self.Ndim):
|
|
1242
|
+
newMeasure += 2**i * np.heaviside(XX[i], 1)
|
|
1243
|
+
if newMeasure > measure:
|
|
1244
|
+
measure = newMeasure
|
|
1245
|
+
Xmax = XX.copy()
|
|
1246
|
+
if measure == maxMeasure:
|
|
1247
|
+
break
|
|
1248
|
+
return Xmax
|
|
1249
|
+
|
|
1250
|
+
def nucleationCriterion(self, S: float, T: float, high_phase, low_phase) -> float:
|
|
1251
|
+
r"""Approximate nucleation criterion
|
|
1252
|
+
|
|
1253
|
+
.. math::
|
|
1254
|
+
\Gamma(T_\mathrm{nuc})/H(T_\mathrm{nuc})^4 = 1
|
|
1255
|
+
|
|
1256
|
+
Return 0 if nuclation is fulfilled
|
|
1257
|
+
|
|
1258
|
+
Parameters
|
|
1259
|
+
----------
|
|
1260
|
+
S : float
|
|
1261
|
+
Action at temperature T
|
|
1262
|
+
T : float
|
|
1263
|
+
Temperature.
|
|
1264
|
+
high_phase : PhaseInfo
|
|
1265
|
+
High temperature phase, start of tunneling
|
|
1266
|
+
low_phase : PhaseInfo
|
|
1267
|
+
Low temperature phase
|
|
1268
|
+
|
|
1269
|
+
Returns
|
|
1270
|
+
-------
|
|
1271
|
+
float
|
|
1272
|
+
Criterion.
|
|
1273
|
+
"""
|
|
1274
|
+
return approxNucleationCriterion(T, S, self, high_phase, low_phase)
|
|
1275
|
+
|
|
1276
|
+
def radiationEnergyDensity(self, X: np.ndarray, T: float, include_decoupled=True) -> float:
|
|
1277
|
+
r"""Return the energy density in radiation that is not
|
|
1278
|
+
field dependent.
|
|
1279
|
+
|
|
1280
|
+
Parameters
|
|
1281
|
+
----------
|
|
1282
|
+
X : np.ndarray
|
|
1283
|
+
The scalar field values
|
|
1284
|
+
T : float
|
|
1285
|
+
Temperature
|
|
1286
|
+
include_decoupled : bool, optional
|
|
1287
|
+
If true, include the enery density of the decoupled radiation bath
|
|
1288
|
+
|
|
1289
|
+
Returns
|
|
1290
|
+
-------
|
|
1291
|
+
float :
|
|
1292
|
+
The energy density in pure radiation at temperature T."""
|
|
1293
|
+
|
|
1294
|
+
# Radiation energy density of particles that interact with the
|
|
1295
|
+
# scalars from the potential
|
|
1296
|
+
T2 = T * T
|
|
1297
|
+
bosons0 = self.boson_massSq(X, 0.0)
|
|
1298
|
+
fermions = self.fermion_massSq(X)
|
|
1299
|
+
m2b, nb, _, _ = bosons0
|
|
1300
|
+
m2f, nf = fermions
|
|
1301
|
+
# This below here is just T dP/dT - P written in terms of the thermal functions
|
|
1302
|
+
VTpart = np.sum(-3 * nb * T2 * T2 * Jb(m2b / T2) + 2 * m2b * nb * T2 * Jb(m2b / T2, n=1), axis=-1)
|
|
1303
|
+
VTpart += np.sum(-3 * nf * T2 * T2 * Jf(m2f / T2) + 2 * m2f * nf * T2 * Jf(m2f / T2, n=1), axis=-1)
|
|
1304
|
+
|
|
1305
|
+
eRadPotential = VTpart / (2 * np.pi**2)
|
|
1306
|
+
|
|
1307
|
+
# Radiation energy density of particles that are neglected in the
|
|
1308
|
+
# effective potential
|
|
1309
|
+
geff = self.kin_coupled_e_geff(T, self.conversionFactor)
|
|
1310
|
+
|
|
1311
|
+
# remove unphysical contributions from the gauge bosons (effect of ghost fields)
|
|
1312
|
+
# in the landau gauge
|
|
1313
|
+
geff -= self.mass_spectrum.number_gauge_bosons
|
|
1314
|
+
|
|
1315
|
+
# Possible additional energy density from decoupled sectors
|
|
1316
|
+
if include_decoupled:
|
|
1317
|
+
geff += self.kin_decoupled_e_geff(T, self.conversionFactor)
|
|
1318
|
+
|
|
1319
|
+
eRadAdditional = np.pi**2 / 30 * geff * T**4
|
|
1320
|
+
|
|
1321
|
+
return eRadPotential + eRadAdditional
|
|
1322
|
+
|
|
1323
|
+
def energyDensityDaisy(self, X: np.ndarray, T: float | np.ndarray):
|
|
1324
|
+
r"""Return the daisy-resummation contribution to the energy density.
|
|
1325
|
+
|
|
1326
|
+
Subclasses with a non-trivial daisy thermal mass can override this.
|
|
1327
|
+
The default is zero and preserves the previous behaviour.
|
|
1328
|
+
"""
|
|
1329
|
+
return np.asarray(X)[..., 0] * 0.0
|
|
1330
|
+
|
|
1331
|
+
def energyDensity(self, X: np.ndarray, T: float | np.ndarray,
|
|
1332
|
+
include_decoupled: bool = True) -> float:
|
|
1333
|
+
r"""Calculate the total energy density.
|
|
1334
|
+
It is important that Vtot includes the field independent
|
|
1335
|
+
terms (i.e. radiation terms which are prop to T^4).
|
|
1336
|
+
|
|
1337
|
+
.. math::
|
|
1338
|
+
e = V - T \partial_T V
|
|
1339
|
+
|
|
1340
|
+
Parameters
|
|
1341
|
+
----------
|
|
1342
|
+
pot : generic_potential
|
|
1343
|
+
Effective potential
|
|
1344
|
+
X : np.ndarray
|
|
1345
|
+
The scalar field values
|
|
1346
|
+
T : float|np.ndarray
|
|
1347
|
+
The temperature at which to compute the energy density
|
|
1348
|
+
include_decoupled : bool, optional
|
|
1349
|
+
If true, include the energy density of the decoupled radiation bath
|
|
1350
|
+
as well.
|
|
1351
|
+
|
|
1352
|
+
Returns
|
|
1353
|
+
-------
|
|
1354
|
+
float|np.ndarray :
|
|
1355
|
+
The energy density at `T`."""
|
|
1356
|
+
|
|
1357
|
+
V0 = self.V0(X) + self.Vct(X) + self.V1_from_X(X)
|
|
1358
|
+
DV0 = V0 - (self.V0(self.X0) + self.Vct(self.X0) + self.V1_from_X(self.X0))
|
|
1359
|
+
# Numerical stability:
|
|
1360
|
+
if DV0/np.abs(V0) < 1e-10:
|
|
1361
|
+
DV0 = 0
|
|
1362
|
+
|
|
1363
|
+
etot = DV0 + self.energyDensityDaisy(X, T)
|
|
1364
|
+
if T != 0.0:
|
|
1365
|
+
etot += self.radiationEnergyDensity(X, T, include_decoupled)
|
|
1366
|
+
return etot
|
|
1367
|
+
|
|
1368
|
+
def makePrettyDictionaryPrint(self, derived_dict: dict={}) -> None:
|
|
1369
|
+
"""Print the potential information in a nice way.
|
|
1370
|
+
|
|
1371
|
+
Parameters
|
|
1372
|
+
----------
|
|
1373
|
+
input_dict : dict
|
|
1374
|
+
Input parameters of the potential
|
|
1375
|
+
derived_dict
|
|
1376
|
+
Parameters derived from the input
|
|
1377
|
+
Returns
|
|
1378
|
+
-------
|
|
1379
|
+
None."""
|
|
1380
|
+
input_table = rich.table.Table(title="Input parameters", title_justify="left", box=rich.box.ROUNDED)
|
|
1381
|
+
input_table.add_column("Parameter", style="cyan", no_wrap=True)
|
|
1382
|
+
input_table.add_column("Value", style="orange1")
|
|
1383
|
+
for n, v in self.mp.items():
|
|
1384
|
+
value = v["value"]
|
|
1385
|
+
input_table.add_row(n, f"{value:.10e}")
|
|
1386
|
+
|
|
1387
|
+
tables = [input_table]
|
|
1388
|
+
|
|
1389
|
+
if derived_dict != {}:
|
|
1390
|
+
derived_table = rich.table.Table(title="Derived parameters", title_justify="left", box=rich.box.ROUNDED)
|
|
1391
|
+
derived_table.add_column("Parameter", style="cyan", no_wrap=True)
|
|
1392
|
+
derived_table.add_column("Value", style="orange1")
|
|
1393
|
+
for n, v in derived_dict.items():
|
|
1394
|
+
derived_table.add_row(n, f"{v:.10e}")
|
|
1395
|
+
|
|
1396
|
+
tables.append(derived_table)
|
|
1397
|
+
|
|
1398
|
+
mass_entries = self.get_zero_temperature_mass_spectrum()
|
|
1399
|
+
|
|
1400
|
+
if mass_entries:
|
|
1401
|
+
mass_table = rich.table.Table(
|
|
1402
|
+
title="Zero-temperature mass spectrum", title_justify="left", box=rich.box.ROUNDED
|
|
1403
|
+
)
|
|
1404
|
+
mass_table.add_column("Particle", style="cyan", no_wrap=True)
|
|
1405
|
+
mass_table.add_column("Type", style="magenta")
|
|
1406
|
+
mass_table.add_column("Mass [GeV]", style="orange1")
|
|
1407
|
+
for entry in mass_entries:
|
|
1408
|
+
text_label = entry.get("text", "")
|
|
1409
|
+
mass_value = entry.get("mass_GeV", np.nan)
|
|
1410
|
+
mass_table.add_row(
|
|
1411
|
+
text_label,
|
|
1412
|
+
entry.get("kind", ""),
|
|
1413
|
+
f"{mass_value:.10e}" if np.isfinite(mass_value) else "-",
|
|
1414
|
+
)
|
|
1415
|
+
tables.append(mass_table)
|
|
1416
|
+
|
|
1417
|
+
console.print(RichColumns(tables))
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
def get_mass_spectrum_T0(self):
|
|
1421
|
+
"""description
|
|
1422
|
+
|
|
1423
|
+
Parameters
|
|
1424
|
+
----------
|
|
1425
|
+
|
|
1426
|
+
Returns
|
|
1427
|
+
-------
|
|
1428
|
+
|
|
1429
|
+
"""
|
|
1430
|
+
spectrum = self.get_mass_spectrum(self.X0, 0.0)
|
|
1431
|
+
counts = {"boson": 0, "fermion": 0}
|
|
1432
|
+
res = []
|
|
1433
|
+
for kind, sector in (("boson", spectrum.bosons), ("fermion", spectrum.fermions)):
|
|
1434
|
+
masses_sq = np.real_if_close(np.asarray(sector.masses_sq))
|
|
1435
|
+
masses_sq = masses_sq.reshape(-1)
|
|
1436
|
+
if masses_sq.size == 0:
|
|
1437
|
+
continue
|
|
1438
|
+
|
|
1439
|
+
latex_labels = sector.latex_labels
|
|
1440
|
+
text_labels = sector.text_labels
|
|
1441
|
+
|
|
1442
|
+
for idx, mass_sq in enumerate(masses_sq):
|
|
1443
|
+
with np.errstate(invalid="ignore"):
|
|
1444
|
+
internal_mass = np.sqrt(mass_sq) if mass_sq >= 0 else np.nan
|
|
1445
|
+
mass_GeV = float(internal_mass * self.conversionFactor) if np.isfinite(internal_mass) else np.nan
|
|
1446
|
+
log_mass = float(np.log10(mass_GeV)) if np.isfinite(mass_GeV) and mass_GeV > 0 else np.nan
|
|
1447
|
+
|
|
1448
|
+
kind_index = counts[kind]
|
|
1449
|
+
counts[kind] += 1
|
|
1450
|
+
|
|
1451
|
+
entry: dict[str, object] = {
|
|
1452
|
+
"kind": kind,
|
|
1453
|
+
"index": kind_index,
|
|
1454
|
+
"mass_GeV": mass_GeV,
|
|
1455
|
+
"log10_mass": log_mass,
|
|
1456
|
+
"latex": latex_labels[idx] if idx < len(latex_labels) else latex_labels[-1] if latex_labels else "",
|
|
1457
|
+
"text": text_labels[idx] if idx < len(text_labels) else text_labels[-1] if text_labels else "",
|
|
1458
|
+
}
|
|
1459
|
+
res.append(entry)
|
|
1460
|
+
|
|
1461
|
+
return res
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def get_zero_temperature_mass_spectrum(self) -> list[dict[str, object]]:
|
|
1465
|
+
"""Return the T=0 mass spectrum in GeV for bosons and fermions."""
|
|
1466
|
+
|
|
1467
|
+
conversion_factor = getattr(self, "conversionFactor", 1.0)
|
|
1468
|
+
try:
|
|
1469
|
+
conversion_factor = float(conversion_factor)
|
|
1470
|
+
except (TypeError, ValueError): # pragma: no cover - defensive guard
|
|
1471
|
+
conversion_factor = 1.0
|
|
1472
|
+
|
|
1473
|
+
X0 = getattr(self, "X0", None)
|
|
1474
|
+
if X0 is None and hasattr(self, "approxZeroTMin"):
|
|
1475
|
+
try:
|
|
1476
|
+
minima = self.approxZeroTMin()
|
|
1477
|
+
except Exception: # pragma: no cover - defensive safeguard
|
|
1478
|
+
minima = None
|
|
1479
|
+
if minima:
|
|
1480
|
+
X0 = minima[0]
|
|
1481
|
+
|
|
1482
|
+
if X0 is None:
|
|
1483
|
+
return []
|
|
1484
|
+
|
|
1485
|
+
try:
|
|
1486
|
+
snapshot = self.get_mass_spectrum(np.asarray(X0), 0.0)
|
|
1487
|
+
except Exception: # pragma: no cover - keep spectrum extraction resilient
|
|
1488
|
+
return []
|
|
1489
|
+
|
|
1490
|
+
results: list[dict[str, object]] = []
|
|
1491
|
+
counts = {"boson": 0, "fermion": 0}
|
|
1492
|
+
|
|
1493
|
+
for kind, sector in (("boson", snapshot.bosons), ("fermion", snapshot.fermions)):
|
|
1494
|
+
masses_sq = np.real_if_close(np.asarray(sector.masses_sq))
|
|
1495
|
+
masses_sq = masses_sq.reshape(-1)
|
|
1496
|
+
if masses_sq.size == 0:
|
|
1497
|
+
continue
|
|
1498
|
+
|
|
1499
|
+
latex_labels = sector.latex_labels
|
|
1500
|
+
text_labels = sector.text_labels
|
|
1501
|
+
|
|
1502
|
+
for idx, mass_sq in enumerate(masses_sq):
|
|
1503
|
+
with np.errstate(invalid="ignore"):
|
|
1504
|
+
internal_mass = np.sqrt(mass_sq) if mass_sq >= 0 else np.nan
|
|
1505
|
+
mass_GeV = float(internal_mass * conversion_factor) if np.isfinite(internal_mass) else np.nan
|
|
1506
|
+
log_mass = float(np.log10(mass_GeV)) if np.isfinite(mass_GeV) and mass_GeV > 0 else np.nan
|
|
1507
|
+
|
|
1508
|
+
kind_index = counts[kind]
|
|
1509
|
+
counts[kind] += 1
|
|
1510
|
+
|
|
1511
|
+
entry: dict[str, object] = {
|
|
1512
|
+
"kind": kind,
|
|
1513
|
+
"index": kind_index,
|
|
1514
|
+
"mass_GeV": mass_GeV,
|
|
1515
|
+
"log10_mass": log_mass,
|
|
1516
|
+
"latex": latex_labels[idx] if idx < len(latex_labels) else latex_labels[-1] if latex_labels else "",
|
|
1517
|
+
"text": text_labels[idx] if idx < len(text_labels) else text_labels[-1] if text_labels else "",
|
|
1518
|
+
}
|
|
1519
|
+
results.append(entry)
|
|
1520
|
+
|
|
1521
|
+
return results
|