TransitionListener 2.0.0__py3-none-any.whl

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