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,1499 @@
1
+ """This module contains the phases object. It contains
2
+ an interface to the phase tracing routines, stores
3
+ the phase information of the potential and provides
4
+ interpolated functions.
5
+
6
+ Part of TransitionListener v2.0
7
+ Documentation: https://tasillo.de/TransitionListener/
8
+
9
+ Authors:
10
+ Jonas Matuszak <jonas.matuszak@kit.edu>
11
+ Carlo Tasillo <carlo.tasillo@ific.uv.es>
12
+ """
13
+
14
+ import numpy as np
15
+ from dataclasses import dataclass
16
+ from scipy import optimize
17
+ from scipy import interpolate
18
+ from scipy import linalg
19
+ from collections import namedtuple
20
+ from typing import TYPE_CHECKING, Callable, Iterable
21
+
22
+ from transitionlistener import generic_potential
23
+ from transitionlistener.errors import WrongHighTPhaseError
24
+
25
+ from . import console, print
26
+ import rich
27
+ from rich.columns import Columns as RichColumns
28
+
29
+ if TYPE_CHECKING:
30
+ from transitionlistener.transitions import TransitionInfo
31
+
32
+ EXTRA_ACCURATE_TRACING = False
33
+
34
+
35
+ def findLocalMinimum(
36
+ X: np.ndarray,
37
+ T: float,
38
+ dV: Callable,
39
+ d2V: Callable,
40
+ phitol: float = 1e-6,
41
+ step_size: float = 0.1,
42
+ ) -> np.ndarray:
43
+ r"""Find a nearby local minimum by Newton stepping in field space."""
44
+
45
+ hess_offset = np.diag(np.ones_like(X) * 1e-5)
46
+ niter = 0
47
+ while True:
48
+ niter += 1
49
+ Hess = d2V(X, T)
50
+ if np.linalg.det(Hess) == 0:
51
+ break
52
+ eps = np.matmul(np.linalg.inv(Hess + hess_offset), dV(X, T))
53
+ X = X - eps * step_size
54
+ if np.sqrt(np.sum(eps * eps)) <= phitol:
55
+ break
56
+ if niter >= 200:
57
+ break
58
+ return X
59
+
60
+
61
+ if EXTRA_ACCURATE_TRACING:
62
+ def traceLocalMinimum(f, x, t, df_dx, d2f_dx2, xeps):
63
+ return findLocalMinimum(x, t, df_dx, d2f_dx2)
64
+ else:
65
+ def traceLocalMinimum(f, x, t, df_dx, d2f_dx2, xeps):
66
+ return optimize.fmin(f, x, args=(t,), xtol=xeps, ftol=np.inf, disp=False)
67
+
68
+
69
+ class PhaseInfo:
70
+ """Describes a temperature-dependent minimum, plus second-order transitions
71
+ to and from that minimum.
72
+
73
+ Attributes
74
+ ----------
75
+ key : hashable
76
+ A unique identifier for the phase (usually an int).
77
+ X, T, dXdT : array_like
78
+ The minima and its derivative at different temperatures.
79
+ tck : tuple
80
+ Spline knots and coefficients, used in `interpolate.splev`.
81
+ low_trans : set
82
+ Phases (identified by keys) which are joined by a second-order
83
+ transition to this phase.
84
+ high_trans : set
85
+ Phases (identified by keys) which are joined by a second-order
86
+ transition to this phase.
87
+ """
88
+
89
+ def __init__(self, key: int | str, X: np.ndarray, T: np.ndarray, dXdT: np.ndarray):
90
+ """Create the spline representation of a phase trajectory in field space.
91
+
92
+ Parameters
93
+ ----------
94
+ key : hashable
95
+ A unique identifier for the phase (usually an int).
96
+ X : np.ndarray
97
+ The minima of the potential at different temperatures.
98
+ T : np.ndarray
99
+ The temperatures at which the minima occur.
100
+ dXdT : np.ndarray
101
+ The derivative of the minima with respect to temperature.
102
+
103
+ .. note::
104
+ This class is not intended to be used directly. Instead, use the
105
+ `Phase` class, which provides a more convenient interface.
106
+
107
+ .. todo::
108
+ Include check if key is hashable. Should it be int or str only?
109
+ """
110
+ self.key = key
111
+ # We shouldn't ever really need to sort the array, but there must be
112
+ # some bug in the above code that makes it so that occasionally the last
113
+ # step goes backwards. This should fix that.
114
+ i = np.argsort(T)
115
+ T, X, dXdT = T[i], X[i], dXdT[i]
116
+ self.X = X
117
+ self.T = T
118
+ self.dXdT = dXdT
119
+ self.Tmax = T[-1]
120
+ self.Tmin = T[0]
121
+ # Make the spline:
122
+ k = 3 if len(T) > 3 else 1
123
+ tck, u = interpolate.splprep(X.T, u=T, s=0, k=k)
124
+ self.tck = tck
125
+ # Make default connections
126
+ self.low_trans = set()
127
+ self.high_trans = set()
128
+ self.parentPhases = [] # empty if high-T phase
129
+ self.childPhases = []
130
+ # if not none, but a phase key, the phase is generated
131
+ # by symmetry transformations of the potential
132
+ self.mirrorPhase = None
133
+
134
+ def valAt(self, T: float | np.ndarray, deriv: int = 0) -> np.ndarray:
135
+ """Find the minimum at the value `T` using a spline.
136
+
137
+ Parameters
138
+ ----------
139
+ T : float | np.ndarray
140
+ The temperature at which to eval the minimum
141
+ deriv : int
142
+ If deriv > 0, instead return the derivative of the minimum with
143
+ respect to `T`. Can return up to the third derivative for cubic
144
+ splines (when ``len(X) > 3``) or first derivative for linear
145
+ splines.
146
+
147
+ Returns
148
+ -------
149
+ np.ndarray :
150
+ The minimum at the temperature `T`."""
151
+ T = np.asanyarray(T).T
152
+ y = interpolate.splev(T, self.tck, der=deriv)
153
+ return np.asanyarray(y).T
154
+
155
+ def addLinkFrom(self, other_phase) -> None:
156
+ """Add a link from `other_phase` to this phase, checking to see if there
157
+ is a second-order transition.
158
+
159
+ Parameters
160
+ ----------
161
+ other_phase : PhaseInfo
162
+ Information of the other phase
163
+ Returns
164
+ -------
165
+ None."""
166
+ if np.min(self.T) >= np.max(other_phase.T):
167
+ self.low_trans.add(other_phase.key)
168
+ other_phase.high_trans.add(self.key)
169
+ if np.max(self.T) <= np.min(other_phase.T):
170
+ self.high_trans.add(other_phase.key)
171
+ other_phase.low_trans.add(self.key)
172
+
173
+ def __repr__(self):
174
+ """Return a concise string representation summarising the phase track."""
175
+ popts = np.get_printoptions()
176
+ np.set_printoptions(formatter={"float": lambda x: "%0.4g" % x})
177
+ if len(self.X) > 1:
178
+ Xstr = "[%s, ..., %s]" % (self.X[0], self.X[-1])
179
+ else:
180
+ Xstr = "[%s]" % self.X[0]
181
+ if len(self.T) > 1:
182
+ Tstr = "[%0.4g, ..., %0.4g]" % (self.T[0], self.T[-1])
183
+ else:
184
+ Tstr = "[%0.4g]" % self.T[0]
185
+ if len(self.dXdT) > 1:
186
+ dXdTstr = "[%s, ..., %s]" % (self.dXdT[0], self.dXdT[-1])
187
+ else:
188
+ dXdTstr = "[%s]" % self.dXdT[0]
189
+ s = "Phase(key=%s, X=%s, T=%s, dXdT=%s" % (self.key, Xstr, Tstr, dXdTstr)
190
+ np.set_printoptions(**popts)
191
+ return s
192
+
193
+
194
+ def findApproxLocalMin(f: Callable, x1: np.ndarray, x2: np.ndarray, args=(), n=100, edge=0.05) -> list:
195
+ """
196
+ Find minima on a straight line between two points.
197
+
198
+ When jumping between phases, we want to make sure that we
199
+ don't jump over an intermediate phase. This function does a rough
200
+ calculation to find any such intermediate phases.
201
+
202
+ Parameters
203
+ ----------
204
+ f : callable
205
+ The function `f(x)` to minimize.
206
+ x1, x2 : array_like
207
+ The points between which to find minima.
208
+ args : tuple, optional
209
+ Extra arguments to pass to `f`.
210
+ n : int, optional
211
+ Number of points to test for local minima.
212
+ edge : float, optional
213
+ Don't test for minima directly next to the input points. If ``edge==0``,
214
+ the minima potentially go all the way to input points. If ``edge==0.5``,
215
+ the range of tested minima shrinks to a single point at the center of
216
+ the two points.
217
+
218
+ Returns
219
+ -------
220
+ list
221
+ A list of approximate minima, with each minimum having the same shape
222
+ as `x1` and `x2`.
223
+ """
224
+ x1, x2 = np.array(x1), np.array(x2)
225
+ x = x1 + (x2 - x1) * np.linspace(edge, 1 - edge, n).reshape(n, 1)
226
+ y = f(x, *args)
227
+ i = (y[2:] > y[1:-1]) & (y[:-2] > y[1:-1])
228
+ return x[1:-1][i]
229
+
230
+
231
+ _traceMinimum_rval = namedtuple("traceMinimum_rval", "X T dXdT overX overT")
232
+
233
+
234
+ def traceMinimum(
235
+ f : Callable, df_dx : Callable, d2f_dxdt : Callable, d2f_dx2 : Callable,
236
+ x0 : np.ndarray, t0 : float, tstop : float, dtstart : float, deltaX_target : float,
237
+ dtabsMax: float = 20.0, dtfracMax: float = 0.25, dtmin : float = 1e-3, deltaX_tol : float = 1.2,
238
+ minratio : float = 1e-2, verbose : bool = False, conversionFactor : float = 1,
239
+ ) -> _traceMinimum_rval:
240
+
241
+ """
242
+ Trace the minimum `xmin(t)` of the function `f(x,t)`, starting at `x0, t0`.
243
+
244
+ Parameters
245
+ ----------
246
+ f : callable
247
+ The scalar function `f(x,t)` which needs to be minimized. The input will
248
+ be of the same type as `(x0,t0)`.
249
+ d2f_dxdt, d2f_dx2 : callable
250
+ Functions which return returns derivatives of `f(x)`. `d2f_dxdt` should
251
+ return the derivative of the gradient of `f(x)` with respect to `t`, and
252
+ `d2f_dx2` should return the Hessian matrix of `f(x)` evaluated at `t`.
253
+ Both should take as inputs `(x,t)`.
254
+ x0 : array_like
255
+ The initial starting point. Must be an array even if the potential is
256
+ one-dimensional (in which case the array should have length 1).
257
+ t0 : float
258
+ The initial starting parameter `t`.
259
+ tstop : float
260
+ Stop the trace when `t` reaches `tstop`.
261
+ dtstart : float
262
+ Initial stepsize.
263
+ deltaX_target : float
264
+ The target error in x at each step. Determines the
265
+ stepsize in t by extrapolation from last error.
266
+ dtabsMax : float, optional
267
+ dtfracMax : float, optional
268
+ The largest stepsize in t will be the LARGEST of
269
+ ``abs(dtstart)*dtabsMax`` and ``t*dtfracMax``.
270
+ dtmin : float, optional
271
+ The smallest stepsize we'll allow before assuming the transition ends,
272
+ relative to `dtstart`
273
+ deltaX_tol : float, optional
274
+ ``deltaX_tol*deltaX_target`` gives the maximum error in x
275
+ before we want to shrink the stepsize and recalculate the minimum.
276
+ minratio : float, optional
277
+ The smallest ratio between smallest and largest eigenvalues in the
278
+ Hessian matrix before treating the smallest eigenvalue as zero (and
279
+ thus signaling a saddle point and the end of the minimum).
280
+
281
+ Returns
282
+ -------
283
+ X, T, dXdT : array_like
284
+ Arrays of the minimum at different values of t, and
285
+ its derivative with respect to t.
286
+ overX : array_like
287
+ The point beyond which the phase seems to disappear.
288
+ overT : float
289
+ The t-value beyond which the phase seems to disappear.
290
+
291
+ Notes
292
+ -----
293
+ In prior versions, `d2f_dx2` was optional and called `d2f`, while `d2f_dxdt`
294
+ was calculated from an optional parameter `df` using finite differences. If
295
+ Neither of these were supplied, they would be calculated directly from
296
+ `f(x,t)` using finite differences. This lead to a messier calling signature,
297
+ since additional parameters were needed to find the finite differences. By
298
+ instead requiring that the derivatives be supplied, the task of creating the
299
+ derivative functions can be delegated to more general purpose routines
300
+ (see e.g. :class:`helper_functions.gradientFunction` and
301
+ :class:`helper_functions.hessianFunction`).
302
+
303
+ Also new in this version, `dtmin` and `dtabsMax` are now relative to
304
+ `dtstart`. The idea here is that there should be some required parameter
305
+ that sets the scale, and then optional parameters can set the tolerances
306
+ relative to this scale. `deltaX_target` is now not optional for the same
307
+ reasoning.
308
+ """
309
+ Ndim = len(x0)
310
+ M0 = d2f_dx2(x0, t0)
311
+ minratio *= min(abs(linalg.eigvalsh(M0))) / max(abs(linalg.eigvalsh(M0)))
312
+
313
+ def dxmindt(x, t):
314
+ M = d2f_dx2(x, t)
315
+ if abs(linalg.det(M)) < (1e-3 * np.max(abs(M))) ** Ndim:
316
+ # Assume matrix is singular
317
+ return None, False
318
+ b = -d2f_dxdt(x, t)
319
+ eigs = linalg.eigvalsh(M)
320
+ try:
321
+ dxdt = linalg.solve(M, b, overwrite_a=False, overwrite_b=False)
322
+ isneg = (eigs <= 0).any() or min(eigs) / max(eigs) < minratio
323
+ except Exception:
324
+ dxdt = None
325
+ isneg = False
326
+ return dxdt, isneg
327
+
328
+ xeps = deltaX_target * 1e-2
329
+
330
+ def fmin(x, t):
331
+ return traceLocalMinimum(f, x, t, df_dx, d2f_dx2, xeps)
332
+
333
+ deltaX_tol = deltaX_tol * deltaX_target
334
+ tscale = abs(dtstart)
335
+ dtabsMax = dtabsMax * tscale
336
+ dtmin = dtmin * tscale
337
+
338
+ x, t, dt, xerr = x0, t0, dtstart, 0.0
339
+ dxdt, negeig = dxmindt(x, t)
340
+ X, T, dXdT = [x], [t], [dxdt]
341
+ overX = overT = None
342
+
343
+ def trace():
344
+ nonlocal x, t, dt, dxdt, xerr, overX, overT
345
+
346
+ # Get the values at the next step
347
+ tnext = t + dt
348
+ xnext = fmin(x + dxdt * dt, tnext)
349
+ dxdt_next, negeig = dxmindt(xnext, tnext)
350
+ if dxdt_next is None or negeig is True:
351
+ # We got stuck on a saddle, so there must be a phase transition
352
+ # there.
353
+ dt *= 0.5
354
+ overX, overT = xnext, tnext
355
+ else:
356
+ # The step might still be too big if it's outside of our error
357
+ # tolerance.
358
+ xerr = max(np.sum((x + dxdt * dt - xnext) ** 2), np.sum((xnext - dxdt_next * dt - x) ** 2)) ** 0.5
359
+ if xerr < deltaX_tol: # Normal step, error is small
360
+ T.append(tnext)
361
+ X.append(xnext)
362
+ dXdT.append(dxdt_next)
363
+ if overT is None:
364
+ # change the stepsize only if the last step wasn't
365
+ # troublesome
366
+ dt *= deltaX_target / (xerr + 1e-100)
367
+ x, t, dxdt = xnext, tnext, dxdt_next
368
+ overX = overT = None
369
+ else:
370
+ # Either stepsize was too big, or we hit a transition.
371
+ # Just cut the step in half.
372
+ dt *= 0.5
373
+ overX, overT = xnext, tnext
374
+
375
+ def check():
376
+ nonlocal x, t, dt, dxdt, xerr, overX, overT, negeig
377
+ # Now do some checks on dt.
378
+ if abs(dt) < abs(dtmin):
379
+ # Found a transition! Or at least a point where the step is really
380
+ # small.
381
+ return True
382
+
383
+ if dt > 0 and t >= tstop or dt < 0 and t <= tstop:
384
+ # Reached tstop, but we want to make sure we stop right at tstop.
385
+ dt = tstop - t
386
+ xnext = fmin(x + dxdt * dt, tstop)
387
+ dxdt_next, negeig = dxmindt(x, tstop)
388
+ tnext = tstop
389
+ if dxdt_next is None:
390
+ # Cannot evaluate the next slope reliably; stop the trace here.
391
+ X[-1], T[-1], dXdT[-1] = xnext, tnext, dxdt
392
+ return True
393
+ # Here it previously doesn't check if we have a phase transition between
394
+ # tstop and t. This can happend. Therefore implement a
395
+ # check for this:
396
+ xerr = max(np.sum((x + dxdt * dt - xnext) ** 2), np.sum((xnext - dxdt_next * dt - x) ** 2)) ** 0.5
397
+ if xerr < deltaX_tol:
398
+ # good step, found endpoint of phase
399
+ X[-1], T[-1], dXdT[-1] = xnext, tnext, dxdt_next
400
+ return True
401
+ else:
402
+ # remove wrong entries:
403
+ _, _, _ = X.pop(), T.pop(), dXdT.pop()
404
+ # reset t, etc
405
+ t, x, _ = T[-1], X[-1], dXdT[-1]
406
+ # Either stepsize was too big, cut the step in half.
407
+ dt *= 0.5
408
+ overX, overT = xnext, tnext
409
+
410
+ dtmax = max(t * dtfracMax, dtabsMax)
411
+ if abs(dt) > dtmax:
412
+ dt = np.sign(dt) * dtmax
413
+ return False
414
+
415
+ if verbose:
416
+ with console.status("Tracing phase...", spinner="bouncingBall"):
417
+ while dxdt is not None:
418
+ trace()
419
+ if check():
420
+ break
421
+ console.print("[bold green]Phase traced :heavy_check_mark:")
422
+ else:
423
+ while dxdt is not None:
424
+ trace()
425
+ if check():
426
+ break
427
+
428
+ if overT is None:
429
+ overX, overT = X[-1], T[-1]
430
+
431
+ X = np.array(X)
432
+ T = np.array(T)
433
+ dXdT = np.array(dXdT)
434
+ return _traceMinimum_rval(X, T, dXdT, overX, overT)
435
+
436
+
437
+ def traceMultiMin(
438
+ f: Callable, df_dx: Callable, d2f_dxdt: Callable, d2f_dx2: Callable,
439
+ points: list, tLow: float, tHigh: float, deltaX_target: float,
440
+ verbose: bool = False, conversionFactor: float = 1, dtstart: float = 1e-3,
441
+ tjump: float = 1e-3, forbidCrit: Callable | None = None, applySymmetries: Callable | None = None,
442
+ single_trace_args: dict = {}, local_min_args: dict = {},
443
+ ) -> dict[int, PhaseInfo]:
444
+ """
445
+ Trace multiple minima `xmin(t)` of the function `f(x,t)`.
446
+
447
+ This function will trace the minima starting from the initial `(x,t)` values
448
+ given in `points`. When a phase disappears, the function will search for
449
+ new nearby minima, and trace them as well. In this way, if each minimum
450
+ corresponds to a different phase, this function can find the (possibly)
451
+ complete phase structure of the potential.
452
+
453
+ Parameters
454
+ ----------
455
+ f : callable
456
+ The scalar function `f(x,t)` which needs to be minimized. The input will
457
+ be of the same type as each entry in the `points` parameter.
458
+ d2f_dxdt, d2f_dx2 : callable
459
+ Functions which return returns derivatives of `f(x)`. `d2f_dxdt` should
460
+ return the derivative of the gradient of `f(x)` with respect to `t`, and
461
+ `d2f_dx2` should return the Hessian matrix of `f(x)` evaluated at `t`.
462
+ Both should take as inputs `(x,t)`.
463
+ points : list
464
+ A list of points [(x1,t1), (x2,t2),...] that we want to trace, where
465
+ `x1`, `x2`, etc. are each a one-dimensional array.
466
+ tLow, tHigh : float
467
+ Lowest and highest temperatures between which to trace.
468
+ deltaX_target : float
469
+ Passed to :func:`traceMinimum` and used to set the tolerance in
470
+ minimization.
471
+ dtstart : float, optional
472
+ The starting stepsize, relative to ``tHigh-tLow``.
473
+ tjump : float, optional
474
+ The jump in `t` from the end of one phase to the initial tracing point
475
+ in another. If this is too large, intermediate phases may be skipped.
476
+ Relative to ``tHigh-tLow``.
477
+ forbidCrit : callable or None, optional
478
+ A function that determines whether or not to forbid a phase with a given
479
+ starting point. Should take a point `x` as input, and return True (if
480
+ the phase should be discarded) or False (if the phase should be kept).
481
+ single_trace_args : dict, optional
482
+ Arguments to pass to :func:`traceMinimum`.
483
+ local_min_args : dict, optoinal
484
+ Arguments to pass to :func:`findApproxLocalMinima`.
485
+
486
+ Returns
487
+ -------
488
+ phases : list[PhaseInfo]
489
+ A dictionary of :class:`Phase` instances. The keys in the dictionary
490
+ are integers corresponding to the order in which the phases were
491
+ constructed.
492
+ """
493
+ # We want the minimization here to be very accurate so that we don't get
494
+ # stuck on a saddle or something. This isn't much of a bottle neck.
495
+ xeps = deltaX_target * 1e-2
496
+ CF = conversionFactor
497
+
498
+ def fmin(x, t):
499
+ return traceLocalMinimum(f, x, t, df_dx, d2f_dx2, xeps)
500
+
501
+ dtstart = dtstart * (tHigh - tLow)
502
+ tjump = tjump * (tHigh - tLow)
503
+ phases = {}
504
+ nextPoint = []
505
+ for p in points:
506
+ x, t = p
507
+ nextPoint.append([t, dtstart, fmin(x, t), None])
508
+
509
+ while len(nextPoint) != 0:
510
+ t1, dt1, x1, linkedFrom = nextPoint.pop()
511
+ # check that we are in the right quadrant of field space.
512
+ if applySymmetries is not None:
513
+ x1 = applySymmetries(x1)
514
+ x1 = fmin(x1, t1) # make sure we start as accurately as possible.
515
+ # Check to see if this point is outside the bounds
516
+ if t1 < tLow or (t1 == tLow and dt1 < 0):
517
+ continue
518
+ if t1 > tHigh or (t1 == tHigh and dt1 > 0):
519
+ continue
520
+ if forbidCrit is not None and forbidCrit(x1) is True:
521
+ continue
522
+ # Check to see if it's redudant with another phase
523
+ for i, phase in enumerate(phases.values()):
524
+ if t1 < min(phase.Tmin, phase.Tmax) or t1 > max(phase.Tmin, phase.Tmax):
525
+ continue
526
+ x = fmin(phase.valAt(t1), t1)
527
+ if np.sum((x - x1) ** 2) ** 0.5 < 2 * deltaX_target:
528
+ # The point is already covered
529
+ # Skip this phase and change the linkage.
530
+ if linkedFrom != i and linkedFrom is not None:
531
+ phase.addLinkFrom(phases[linkedFrom])
532
+ break
533
+ else:
534
+ # The point is not yet covered. Trace the phase.
535
+ if verbose:
536
+ print("\nTracing phase starting at phi =", x1 * CF, "GeV; T =", t1 * CF, "GeV")
537
+ phase_key = len(phases)
538
+ oldNumPoints = len(nextPoint)
539
+ if t1 > tLow:
540
+ if verbose:
541
+ print("Tracing minimum down")
542
+ down_trace = traceMinimum(
543
+ f,
544
+ df_dx,
545
+ d2f_dxdt,
546
+ d2f_dx2,
547
+ x1,
548
+ t1,
549
+ tLow,
550
+ -dt1,
551
+ deltaX_target,
552
+ **single_trace_args,
553
+ verbose=verbose,
554
+ conversionFactor=conversionFactor,
555
+ )
556
+ X_down, T_down, dXdT_down, nX, nT = down_trace
557
+ t2, dt2 = nT - tjump, 0.1 * tjump
558
+ x2 = fmin(nX, t2)
559
+ nextPoint.append([t2, dt2, x2, phase_key])
560
+ if np.sum((X_down[-1] - x2) ** 2) > deltaX_target**2:
561
+ for point in findApproxLocalMin(
562
+ f, X_down[-1], x2, (t2,), **local_min_args
563
+ ):
564
+ nextPoint.append([t2, dt2, fmin(point, t2), phase_key])
565
+ X_down = X_down[::-1]
566
+ T_down = T_down[::-1]
567
+ dXdT_down = dXdT_down[::-1]
568
+ if t1 < tHigh:
569
+ if verbose:
570
+ print("Tracing minimum up")
571
+ up_trace = traceMinimum(
572
+ f,
573
+ df_dx,
574
+ d2f_dxdt,
575
+ d2f_dx2,
576
+ x1,
577
+ t1,
578
+ tHigh,
579
+ +dt1,
580
+ deltaX_target,
581
+ **single_trace_args,
582
+ verbose=verbose,
583
+ conversionFactor=conversionFactor,
584
+ )
585
+ X_up, T_up, dXdT_up, nX, nT = up_trace
586
+ t2, dt2 = nT + tjump, 0.1 * tjump
587
+ x2 = fmin(nX, t2)
588
+ nextPoint.append([t2, dt2, x2, phase_key])
589
+ if np.sum((X_up[-1] - x2) ** 2) > deltaX_target**2:
590
+ for point in findApproxLocalMin(
591
+ f, X_up[-1], x2, (t2,), **local_min_args
592
+ ):
593
+ nextPoint.append([t2, dt2, fmin(point, t2), phase_key])
594
+ # Then join the two together
595
+ if t1 <= tLow:
596
+ X, T, dXdT = X_up, T_up, dXdT_up
597
+ elif t1 >= tHigh:
598
+ X, T, dXdT = X_down, T_down, dXdT_down
599
+ else:
600
+ X = np.append(X_down, X_up[1:], 0)
601
+ T = np.append(T_down, T_up[1:], 0)
602
+ dXdT = np.append(dXdT_down, dXdT_up[1:], 0)
603
+ if forbidCrit is not None and (forbidCrit(X[0]) or forbidCrit(X[-1])):
604
+ # The phase is forbidden.
605
+ # Don't add it, and make it a dead-end.
606
+ nextPoint = nextPoint[:oldNumPoints]
607
+ elif len(X) > 1:
608
+ newphase = PhaseInfo(phase_key, X, T, dXdT)
609
+ if linkedFrom is not None:
610
+ newphase.addLinkFrom(phases[linkedFrom])
611
+ phases[phase_key] = newphase
612
+ else:
613
+ # The phase is just a single point.
614
+ # Don't add it, and make it a dead-end.
615
+ nextPoint = nextPoint[:oldNumPoints]
616
+
617
+ return phases
618
+
619
+
620
+ def generateMirrorPhases(phases: dict[int | str, PhaseInfo],
621
+ diftol: float, invGroupElements: list[np.ndarray]):
622
+ """Use the transformations of the potential
623
+ to generate the mirror phases that have not been traced.
624
+
625
+ Parameters
626
+ ----------
627
+ phases : list[PhaseInfo]
628
+ List of the phases
629
+ diftol : float
630
+ The tolerance in field space for which to consider two phases equal
631
+ invGroupElements : list[np.ndarray]
632
+ Matrix transformations which leave the potential invariant
633
+
634
+ Returns
635
+ -------
636
+ """
637
+ if invGroupElements == []:
638
+ return None
639
+
640
+ mPhases = []
641
+
642
+ for phase in phases.values():
643
+ new_mphases = []
644
+ for i, g in enumerate(invGroupElements):
645
+ if np.sum(g) == len(g):
646
+ # skip the identity
647
+ break
648
+ mkey = str(phase.key) + "-m" + str(i + 1)
649
+ mirrorPhase = PhaseInfo(mkey, phase.X @ g, phase.T, phase.dXdT @ g)
650
+ for lk in phase.low_trans:
651
+ mirrorPhase.low_trans.add(str(lk) + "-m" + str(i))
652
+ for hk in phase.high_trans:
653
+ mirrorPhase.high_trans.add(str(lk) + "-m" + str(i))
654
+
655
+ mirrorPhase.mirrorPhase = phase.key
656
+ new_mphases.append(mirrorPhase)
657
+
658
+ while True:
659
+ redundant = False
660
+ mp = new_mphases.pop()
661
+ # check if we created one phase twice by 2 different
662
+ # transformations
663
+ for op in new_mphases:
664
+ DXmin = mp.X[0] - op.X[0]
665
+ DXmax = mp.X[-1] - op.X[-1]
666
+ if (np.sqrt(np.dot(DXmin, DXmin)) < diftol and
667
+ np.sqrt(np.dot(DXmax, DXmax)) < diftol):
668
+ redundant = True
669
+ break
670
+
671
+ if not redundant:
672
+ mPhases.append(mp)
673
+ if new_mphases == []:
674
+ break
675
+
676
+ for mp in mPhases:
677
+ phases[mp.key] = mp
678
+
679
+
680
+ def _removeRedundantPhase(phases: dict[int | str, PhaseInfo],
681
+ removed_phase: PhaseInfo,
682
+ redundant_with_phase: PhaseInfo):
683
+ """
684
+ Remove a phase from the dictionary of phases, and update the links
685
+ between the remaining phases.
686
+
687
+ Parameters
688
+ ----------
689
+ phases : dict
690
+ The dictionary of phases.
691
+
692
+ removed_phase : PhaseInfo
693
+ The phase to be removed.
694
+
695
+ redundant_with_phase : PhaseInfo
696
+ The phase with which the removed phase is redundant.
697
+ """
698
+ for key in removed_phase.low_trans:
699
+ if key != redundant_with_phase.key:
700
+ p = phases[key]
701
+ p.high_trans.discard(removed_phase.key)
702
+ redundant_with_phase.addLinkFrom(p)
703
+
704
+ for key in removed_phase.high_trans:
705
+ if key != redundant_with_phase.key:
706
+ p = phases[key]
707
+ p.low_trans.discard(removed_phase.key)
708
+ redundant_with_phase.addLinkFrom(p)
709
+ del phases[removed_phase.key]
710
+
711
+
712
+ @dataclass
713
+ class PhaseComparison:
714
+ """Summary information about the overlap between two phases.
715
+
716
+ The object stores the temperature interval where the phases overlap and the
717
+ corresponding positions of the minima. This allows helper routines to
718
+ decide whether phases should be merged or kept separate without re-running
719
+ the minimiser.
720
+ """
721
+
722
+ phase1: PhaseInfo
723
+ phase2: PhaseInfo
724
+ tmin: float
725
+ tmax: float
726
+ x1_min: np.ndarray
727
+ x2_min: np.ndarray
728
+ x1_max: np.ndarray
729
+ x2_max: np.ndarray
730
+ same_at_tmin: bool
731
+ same_at_tmax: bool
732
+
733
+
734
+ def _phase_overlap_summary(
735
+ phase1: PhaseInfo,
736
+ phase2: PhaseInfo,
737
+ fmin: Callable[[np.ndarray, float], np.ndarray],
738
+ diftol: float,
739
+ verbose: bool,
740
+ conversionFactor: float,
741
+ ) -> PhaseComparison | None:
742
+ """Compute the overlap and equality checks between two phases."""
743
+ tmax = min(phase1.Tmax, phase2.Tmax) # lowest of the two highest temperatures
744
+ tmin = max(phase1.Tmin, phase2.Tmin) # highest of the two lowest temperatures
745
+ if tmin > tmax: # no overlap in the phases
746
+ return None
747
+
748
+ # Last point in the phase 1
749
+ x1_max = phase1.X[-1] if tmax == phase1.Tmax else fmin(phase1.valAt(tmax), tmax)
750
+ # Last point in the phase 2
751
+ x2_max = phase2.X[-1] if tmax == phase2.Tmax else fmin(phase2.valAt(tmax), tmax)
752
+ dif_max = np.linalg.norm(x1_max - x2_max)
753
+ same_at_tmax = dif_max < diftol
754
+
755
+ # First point in the phase 1
756
+ x1_min = phase1.X[0] if tmin == phase1.Tmin else fmin(phase1.valAt(tmin), tmin)
757
+ # First point in the phase 2
758
+ x2_min = phase2.X[0] if tmin == phase2.Tmin else fmin(phase2.valAt(tmin), tmin)
759
+ dif_min = np.linalg.norm(x1_min - x2_min)
760
+ same_at_tmin = dif_min < diftol
761
+
762
+ if verbose:
763
+ print("Tmax = {:} GeV".format(tmax * conversionFactor))
764
+ print("Phase {:} at Tmax = {:} GeV".format(phase1.key, x1_max * conversionFactor))
765
+ print("Phase {:} at Tmax = {:} GeV".format(phase2.key, x2_max * conversionFactor))
766
+ print(
767
+ "Difference between phase {:} and {:} at Tmax = {:} GeV".format(
768
+ phase1.key, phase2.key, dif_max * conversionFactor
769
+ )
770
+ )
771
+ if same_at_tmax:
772
+ print(
773
+ "This is smaller than the tolerance",
774
+ diftol * conversionFactor,
775
+ "GeV. The phases are considered equal.\n",
776
+ )
777
+ else:
778
+ print("This is larger than the tolerance", diftol * conversionFactor, "GeV.\n")
779
+
780
+ print("Tmin = {:} GeV".format(tmin * conversionFactor))
781
+ print("Phase {:} at Tmin = {:} GeV".format(phase1.key, x1_min * conversionFactor))
782
+ print("Phase {:} at Tmin = {:} GeV".format(phase2.key, x2_min * conversionFactor))
783
+ print(
784
+ "Difference between phase {:} and {:} at Tmin = {:} GeV".format(
785
+ phase1.key, phase2.key, dif_min * conversionFactor
786
+ )
787
+ )
788
+ if same_at_tmin:
789
+ print(
790
+ "This is smaller than the tolerance",
791
+ diftol * conversionFactor,
792
+ "GeV. The phases are considered equal.\n",
793
+ )
794
+ else:
795
+ print("This is larger than the tolerance", diftol * conversionFactor, "GeV.\n")
796
+
797
+ return PhaseComparison(
798
+ phase1=phase1,
799
+ phase2=phase2,
800
+ tmin=tmin,
801
+ tmax=tmax,
802
+ x1_min=x1_min,
803
+ x2_min=x2_min,
804
+ x1_max=x1_max,
805
+ x2_max=x2_max,
806
+ same_at_tmin=same_at_tmin,
807
+ same_at_tmax=same_at_tmax,
808
+ )
809
+
810
+
811
+ def _merge_full_overlap(
812
+ phases: dict[int | str, PhaseInfo],
813
+ comp: PhaseComparison,
814
+ verbose: bool,
815
+ ) -> bool:
816
+ """Merge two fully overlapping phases when they coincide at tmin and tmax.
817
+
818
+ This mirrors the original CosmoTransitions logic: if two phases agree
819
+ throughout their overlap, keep a single representative that stretches
820
+ across the combined temperature range. The verbose block recreates the
821
+ informative output from the previous implementation.
822
+ """
823
+ if not (comp.same_at_tmin and comp.same_at_tmax):
824
+ return False
825
+
826
+ if verbose:
827
+ print("Found two phases that are the same at tmin and tmax.")
828
+ print("Phases {:} and {:} are redundant.\n".format(comp.phase1.key, comp.phase2.key))
829
+
830
+ p_low = comp.phase1 if comp.phase1.Tmin <= comp.phase2.Tmin else comp.phase2
831
+ p_high = comp.phase1 if comp.phase1.Tmax > comp.phase2.Tmax else comp.phase2
832
+
833
+ if p_low is p_high:
834
+ p_reject = comp.phase1 if p_low is comp.phase2 else comp.phase2
835
+ _removeRedundantPhase(phases, p_reject, p_low)
836
+ return True
837
+
838
+ mask_low = p_low.T <= comp.tmax
839
+ T_low = p_low.T[mask_low]
840
+ X_low = p_low.X[mask_low]
841
+ dXdT_low = p_low.dXdT[mask_low]
842
+
843
+ mask_high = p_high.T > comp.tmax
844
+ T_high = p_high.T[mask_high]
845
+ X_high = p_high.X[mask_high]
846
+ dXdT_high = p_high.dXdT[mask_high]
847
+
848
+ T = np.append(T_low, T_high, axis=0)
849
+ X = np.append(X_low, X_high, axis=0)
850
+ dXdT = np.append(dXdT_low, dXdT_high, axis=0)
851
+
852
+ newkey = f"{p_low.key}-{p_high.key}"
853
+ newphase = PhaseInfo(newkey, X, T, dXdT)
854
+ phases[newkey] = newphase
855
+ _removeRedundantPhase(phases, p_low, newphase)
856
+ _removeRedundantPhase(phases, p_high, newphase)
857
+ return True
858
+
859
+
860
+ def _merge_branch_overlap(
861
+ phases: dict[int | str, PhaseInfo],
862
+ comp: PhaseComparison,
863
+ verbose: bool,
864
+ conversionFactor: float,
865
+ ) -> bool:
866
+ """Handle the case where phases coincide at tmax but differ at tmin."""
867
+ if not comp.same_at_tmax or comp.same_at_tmin:
868
+ return False
869
+
870
+ p_low = comp.phase1 if comp.phase1.Tmin <= comp.phase2.Tmin else comp.phase2
871
+ p_high = comp.phase1 if comp.phase1.Tmax > comp.phase2.Tmax else comp.phase2
872
+
873
+ if verbose:
874
+ msg = (
875
+ f"The phases {p_low.key} and {p_high.key} merge at "
876
+ f"T = {comp.tmax * conversionFactor} GeV. "
877
+ "They are identical there, but develop into two different branches at lower temperatures. "
878
+ "This might lead to domain walls, which our code cannot handle yet. "
879
+ "We just remove the branch which dies the quickest.\n"
880
+ )
881
+ console.print(f"[bold yellow]WARNING:[/bold yellow] {msg}")
882
+
883
+ mask_low = p_low.T <= comp.tmax
884
+ T_low = p_low.T[mask_low]
885
+ X_low = p_low.X[mask_low]
886
+ dXdT_low = p_low.dXdT[mask_low]
887
+
888
+ mask_high = p_high.T > comp.tmax
889
+ T_high = p_high.T[mask_high]
890
+ X_high = p_high.X[mask_high]
891
+ dXdT_high = p_high.dXdT[mask_high]
892
+
893
+ T = np.append(T_low, T_high, axis=0)
894
+ X = np.append(X_low, X_high, axis=0)
895
+ dXdT = np.append(dXdT_low, dXdT_high, axis=0)
896
+
897
+ newkey = f"{p_low.key}-{p_high.key}"
898
+ newphase = PhaseInfo(newkey, X, T, dXdT)
899
+ phases[newkey] = newphase
900
+ _removeRedundantPhase(phases, comp.phase1, newphase)
901
+ _removeRedundantPhase(phases, comp.phase2, newphase)
902
+ return True
903
+
904
+
905
+ def removeRedundantPhases(
906
+ f: Callable,
907
+ df_dx: np.ndarray,
908
+ d2f_dx2: np.ndarray,
909
+ phases: dict[int | str, PhaseInfo],
910
+ xeps=1e-5,
911
+ diftol=1e-2,
912
+ verbose=False,
913
+ conversionFactor=1,
914
+ ):
915
+ """
916
+ Remove redundant phases from a dictionary output by :func:`traceMultiMin`.
917
+
918
+ Although :func:`traceMultiMin` attempts to only trace each phase once, there
919
+ are still instances where a single phase gets traced twice. If a phase is
920
+ included twice, the routines for finding transition regions and tunnelling
921
+ get very confused. This routine follows the original CosmoTransitions
922
+ philosophy while splitting the individual steps into small helpers so they
923
+ can be tested independently.
924
+
925
+ Parameters
926
+ ----------
927
+ f : callable
928
+ The scalar potential ``f(x, T)`` that was passed to :func:`traceMultiMin`.
929
+ df_dx, d2f_dx2 : np.ndarray
930
+ First and second derivatives of ``f`` with respect to the fields.
931
+ phases : dict
932
+ Dictionary of :class:`PhaseInfo` objects returned by :func:`traceMultiMin`.
933
+ xeps : float, optional
934
+ Minimisation tolerance in field space when re-minimising at the overlap.
935
+ diftol : float, optional
936
+ Maximum separation between potential minima before they are considered identical.
937
+ verbose : bool, optional
938
+ Emit diagnostic output that mirrors the original implementation.
939
+ conversionFactor : float, optional
940
+ Conversion factor from internal units to GeV for verbose messages.
941
+
942
+ Notes
943
+ -----
944
+ If two phases are merged to get rid of redundancy, the resulting phase has
945
+ a key that is a string combination of the two prior keys.
946
+ """
947
+
948
+ # We want to make the logic extremely simple at the cost of checking the
949
+ # same thing multiple times.
950
+ # There's just no way this function is going to be the bottle neck.
951
+
952
+ def fmin(x, t): # noqa: ANN202 - NumPy array signature is clear from context
953
+ return np.array(traceLocalMinimum(f, x, t, df_dx, d2f_dx2, xeps))
954
+
955
+ while True:
956
+ merged = False # Track whether any change was made in this pass
957
+ phase_list = list(phases.values())
958
+ for phase1 in phase_list:
959
+ for phase2 in phase_list:
960
+ if phase1.key == phase2.key:
961
+ continue
962
+ summary = _phase_overlap_summary(
963
+ phase1,
964
+ phase2,
965
+ fmin,
966
+ diftol,
967
+ verbose,
968
+ conversionFactor,
969
+ )
970
+ if summary is None:
971
+ continue
972
+ if _merge_full_overlap(phases, summary, verbose):
973
+ merged = True
974
+ break
975
+ if _merge_branch_overlap(phases, summary, verbose, conversionFactor):
976
+ merged = True
977
+ break
978
+ if summary.same_at_tmin:
979
+ # These phases may still allow tunneling even if they merge
980
+ # at lower temperatures, so keep both.
981
+ pass
982
+ if merged:
983
+ break
984
+ if not merged:
985
+ break
986
+
987
+
988
+ class Phases:
989
+ """This class contains the phase tracing routines
990
+ and stores the phase information.
991
+ """
992
+
993
+ def __init__(self, pot: generic_potential, verbose=False):
994
+ """Trace all phases of ``pot`` and build their transition graph."""
995
+ self.pot = pot
996
+ self.verbose = verbose
997
+ self.Tmin = pot.Tmin
998
+ self.Tmax = pot.Tmax
999
+ self.x_eps = pot.config.tracingConf.tracing_field_accuracy
1000
+ self.T_eps = pot.config.tracingConf.tracing_temp_accuracy
1001
+ self.tracingArgs = pot.config.tracingConf.tracing_args
1002
+ self.genMirrorPhases = pot.config.tracingConf.gen_mirror_phases
1003
+ self.diftol = pot.config.tracingConf.diftol
1004
+ self.conversionFactor = pot.conversionFactor
1005
+ self.rootPhase = None
1006
+ self.T0Phase = None
1007
+
1008
+ self.phases = self.findPhases(self.genMirrorPhases)
1009
+ self.testTmax()
1010
+ self.buildPhaseGraph(self.phases, pot.Vtot)
1011
+ if verbose:
1012
+ console.print("\nFound {:} phases:\n".format(len(self.phases)))
1013
+ console.print(self.__str__())
1014
+
1015
+ def __str__(self):
1016
+ """Return a formatted overview of all phases and their temperature range."""
1017
+ tables = []
1018
+ for i in self.phases.keys():
1019
+ table = rich.table.Table(title="Phase " + str(i), title_justify="left", box=rich.box.ROUNDED)
1020
+ table.add_column("Parameter", style="cyan", no_wrap=True)
1021
+ table.add_column("Value", style="orange1")
1022
+ table.add_row("Tmin / GeV", str(self.phases[i].Tmin * self.conversionFactor))
1023
+ table.add_row("Tmax / GeV", str(self.phases[i].Tmax * self.conversionFactor))
1024
+ table.add_row("phi(Tmin) / GeV", str(self.phases[i].X[0] * self.conversionFactor))
1025
+ table.add_row("phi(Tmax) / GeV", str(self.phases[i].X[-1] * self.conversionFactor))
1026
+ tables.append(table)
1027
+ console.print(RichColumns(tables))
1028
+ return ""
1029
+
1030
+ def __getitem__(self, key: str) -> PhaseInfo:
1031
+ """Return the :class:`PhaseInfo` object associated with ``key``."""
1032
+ return self.phases[key]
1033
+
1034
+ def keys(self) -> list[str]:
1035
+ """Expose the keys of the internally stored phases."""
1036
+ return self.phases.keys()
1037
+
1038
+ def phase_alias(self, key: int | str) -> str:
1039
+ """Return a short user-facing alias such as ``P1`` for one phase key."""
1040
+ for idx, candidate in enumerate(self.phases.keys(), start=1):
1041
+ if candidate == key:
1042
+ return f"P{idx}"
1043
+ return str(key)
1044
+
1045
+ def resolve_phase_key(self, key_or_alias: int | str) -> int | str:
1046
+ """Resolve a user-facing alias such as ``P1`` back to the internal key."""
1047
+ if isinstance(key_or_alias, str):
1048
+ alias = key_or_alias.strip().upper()
1049
+ if alias.startswith("P") and alias[1:].isdigit():
1050
+ idx = int(alias[1:]) - 1
1051
+ keys = list(self.phases.keys())
1052
+ if 0 <= idx < len(keys):
1053
+ return keys[idx]
1054
+ return key_or_alias
1055
+
1056
+ def __len__(self) -> int:
1057
+ """Return the number of phases that were traced."""
1058
+ return len(self.phases)
1059
+
1060
+ def testTmax(self):
1061
+ """Test that there is only one phase with Tmax = self.Tmax
1062
+
1063
+ Raises
1064
+ ------
1065
+ ValueError
1066
+ If more than one phase has Tmax = self.Tmax
1067
+ """
1068
+ Tmax_phases = []
1069
+ for p in self.phases.values():
1070
+ if abs(p.Tmax - self.Tmax) < self.T_eps:
1071
+ Tmax_phases.append(p.key)
1072
+ if len(Tmax_phases) > 1:
1073
+ msg = f"More than one phase has Tmax = {self.Tmax * self.conversionFactor} GeV: {Tmax_phases}. "
1074
+ msg += "This is not allowed. Please increase the Tmax_factor in the model file."
1075
+ raise WrongHighTPhaseError(msg)
1076
+
1077
+ def printPhaseTree(self):
1078
+ """description
1079
+
1080
+ Parameters
1081
+ ----------
1082
+
1083
+ Returns
1084
+ -------
1085
+
1086
+ """
1087
+ rootPhase = self.rootPhase
1088
+ print(f"High-T Phase = {rootPhase}")
1089
+ for p in self.phases.values():
1090
+ print(f"Phase {p.key} -> {p.childPhases}")
1091
+
1092
+ def findPhases(self, genMirrorPhases: bool = True) -> list[PhaseInfo]:
1093
+ """Find different phases as functions of temperature
1094
+
1095
+ Parameters
1096
+ ----------
1097
+ genMirrorPhases : bool, optional
1098
+ If true, generate phases by applying the symmetry transformations
1099
+ of the potential to the existing phases.
1100
+
1101
+ Returns
1102
+ -------
1103
+ list[PhaseInfo]
1104
+ Each item in the returned dictionary is an instance of
1105
+ :class:`transitionFinder.Phase`, and each phase is
1106
+ identified by a unique key. This value is also stored in
1107
+ `self.phases`.
1108
+ """
1109
+ pot = self.pot
1110
+ verbose = self.verbose
1111
+ # `tracing_args` is primarily forwarded to `traceMinimum` via
1112
+ # `single_trace_args`, but we also allow selected `traceMultiMin`
1113
+ # controls (dtstart/tjump/local_min_args) to be configured from the
1114
+ # same model-level dictionary.
1115
+ single_trace_args = dict(self.tracingArgs)
1116
+ trace_multi_overrides = {}
1117
+ for key in ("dtstart", "tjump", "local_min_args"):
1118
+ if key in single_trace_args:
1119
+ trace_multi_overrides[key] = single_trace_args.pop(key)
1120
+
1121
+ tracingArgs = dict(
1122
+ single_trace_args=single_trace_args,
1123
+ forbidCrit=self.pot.forbidPhaseCrit,
1124
+ **trace_multi_overrides,
1125
+ )
1126
+ tstart = self.Tmin
1127
+ tstop = self.Tmax
1128
+
1129
+ points = []
1130
+ for x0 in pot.approxZeroTMin():
1131
+ points.append([x0, tstart])
1132
+ tracingArgs_ = dict(forbidCrit=pot.forbidPhaseCrit)
1133
+ tracingArgs_.update(tracingArgs)
1134
+
1135
+ if verbose:
1136
+ console.print(
1137
+ f"\nPhase tracing between T = {tstart * self.conversionFactor}" +
1138
+ f" GeV and T = {tstop * self.conversionFactor} GeV",
1139
+ style="bold green",
1140
+ )
1141
+
1142
+ def V_(X, T):
1143
+ # speedup: no call to interpolated geff needed here
1144
+ return pot.Vtot(X, T, include_radiation=False, include_decoupled=False)
1145
+
1146
+ phases = traceMultiMin(
1147
+ V_, # pot.Vtot,
1148
+ pot.gradV,
1149
+ pot.dgradV_dT,
1150
+ pot.d2V,
1151
+ points,
1152
+ verbose=verbose,
1153
+ conversionFactor=self.conversionFactor,
1154
+ applySymmetries=pot.applySymmetries,
1155
+ tLow=tstart,
1156
+ tHigh=tstop,
1157
+ deltaX_target=100 * self.x_eps,
1158
+ **tracingArgs_,
1159
+ )
1160
+
1161
+ if verbose:
1162
+ console.print(f"\nInitial tracing found {len(phases)} phases."
1163
+ f" Removing redundant phases...",
1164
+ style="bold green")
1165
+
1166
+
1167
+ removeRedundantPhases(
1168
+ pot.Vtot, pot.gradV, pot.d2V, phases, self.x_eps * 1e-2, diftol=self.diftol,
1169
+ verbose=verbose, conversionFactor=self.conversionFactor
1170
+ )
1171
+
1172
+ if verbose:
1173
+ console.print("After removing redundant phases, {:} phases remain.".format(len(phases)),
1174
+ style="bold green")
1175
+
1176
+ if genMirrorPhases:
1177
+ generateMirrorPhases(phases, self.diftol, pot.invGroupElements)
1178
+ return phases
1179
+
1180
+ def buildPhaseGraph(self, in_phases: dict[int | str, PhaseInfo], V: Callable):
1181
+ """Construct a tree of the phase history starting from the high
1182
+ temperature phase.
1183
+
1184
+ Parameters
1185
+ ----------
1186
+ in_phases : list[PhaseInfo]
1187
+ List of phases
1188
+ V : callable
1189
+ The temperature dependent effective potential V(X, T)
1190
+
1191
+ Returns
1192
+ -------
1193
+ None."""
1194
+ # First find the start and end phases
1195
+ rootPhase = self.getStartPhase(in_phases, V)
1196
+ self.rootPhase = rootPhase
1197
+ T0Phase = self.getT0Phase(in_phases, V)
1198
+ self.T0Phase = T0Phase
1199
+
1200
+ phases = []
1201
+ for key in in_phases.keys():
1202
+ phases.append(key)
1203
+ for startPhase in phases:
1204
+ for endPhase in phases:
1205
+ if startPhase == endPhase:
1206
+ continue
1207
+
1208
+ tmax = min(in_phases[startPhase].Tmax, in_phases[endPhase].Tmax)
1209
+ tmin = max(in_phases[startPhase].Tmin, in_phases[endPhase].Tmin)
1210
+
1211
+ if tmin >= tmax:
1212
+ # no overlap, except if 2nd order transition
1213
+ if in_phases[endPhase].key in in_phases[startPhase].low_trans:
1214
+ in_phases[endPhase].parentPhase = startPhase
1215
+ in_phases[startPhase].childPhases.append(endPhase)
1216
+ continue
1217
+
1218
+ def DV(T):
1219
+ return V(in_phases[startPhase].valAt(T), T) - V(in_phases[endPhase].valAt(T), T)
1220
+
1221
+ DVmin = DV(tmin)
1222
+
1223
+ if DVmin < 0:
1224
+ # No tunneling possible
1225
+ continue
1226
+
1227
+ in_phases[startPhase].childPhases.append(endPhase)
1228
+ in_phases[endPhase].parentPhases.append(startPhase)
1229
+
1230
+ def getStartPhase(self, phases: dict[int | str, PhaseInfo], V: Callable = None) -> int | str:
1231
+ """
1232
+ Find the key for the high-T phase.
1233
+
1234
+ Parameters
1235
+ ----------
1236
+ phases : dict
1237
+ Output from :func:`traceMultiMin`.
1238
+ V : callable
1239
+ The potential V(x,T). Only necessary if there are
1240
+ multiple phases with the same Tmax.
1241
+
1242
+ Returns
1243
+ -------
1244
+ int | str :
1245
+ The key of the high-T phase.
1246
+ """
1247
+ startPhases = []
1248
+ startPhase = None
1249
+ Tmax = None
1250
+ assert len(phases) > 0
1251
+ for i in list(phases.keys()):
1252
+ if phases[i].T[-1] == Tmax:
1253
+ # add this to the startPhases list.
1254
+ startPhases.append(i)
1255
+ elif Tmax is None or phases[i].T[-1] > Tmax:
1256
+ startPhases = [i]
1257
+ Tmax = phases[i].T[-1]
1258
+ if len(startPhases) == 1 or V is None:
1259
+ startPhase = startPhases[0]
1260
+ else:
1261
+ # more than one phase have the same maximum temperature
1262
+ # Pick the stable one at high temp.
1263
+ Vmin = None
1264
+ for i in startPhases:
1265
+ V_ = V(phases[i].X[-1], phases[i].T[-1])
1266
+ if Vmin is None or V_ < Vmin:
1267
+ Vmin = V_
1268
+ startPhase = i
1269
+ assert startPhase in phases
1270
+ return startPhase
1271
+
1272
+ def getT0Phase(self, phases : dict[int | str, PhaseInfo], V: Callable) -> int | str:
1273
+ """Find the stable phase at T = 0.
1274
+
1275
+ Parameters
1276
+ ----------
1277
+ phases : list[PhaseInfo]
1278
+ List of the phases
1279
+ V : callable
1280
+ The effective potential as V(X, T)
1281
+
1282
+ Returns
1283
+ -------
1284
+ int | str:
1285
+ The key of the stable phase at T = 0."""
1286
+ endPhases = []
1287
+ endPhase = None
1288
+ Tmin = None
1289
+ assert len(phases) > 0
1290
+ for i in list(phases.keys()):
1291
+ if Tmin is None or phases[i].Tmin < Tmin:
1292
+ Tmin = phases[i].Tmin
1293
+
1294
+ for i in list(phases.keys()):
1295
+ if phases[i].Tmin == Tmin:
1296
+ endPhases.append(i)
1297
+
1298
+ if len(endPhases) == 1:
1299
+ endPhase = endPhases[0]
1300
+ else:
1301
+ Vmin = None
1302
+ for i in endPhases:
1303
+ V_ = V(phases[i].valAt(Tmin), Tmin)
1304
+ if Vmin is None or V_ < Vmin:
1305
+ Vmin = V_
1306
+ endPhase = i
1307
+ assert endPhase in phases
1308
+ return endPhase
1309
+
1310
+ # ==================================================
1311
+ # Routines for plotting the phase history
1312
+ # ==================================================
1313
+
1314
+
1315
+ @dataclass(frozen=True)
1316
+ class PhaseSegment:
1317
+ """Piecewise representation of the thermal history across a single phase."""
1318
+
1319
+ index: int
1320
+ phase_key: int | str
1321
+ phase: PhaseInfo
1322
+ entry_temperature: float
1323
+ exit_temperature: float
1324
+
1325
+ def bounds(self) -> tuple[float, float]:
1326
+ """Return the temperature interval for which this phase is active."""
1327
+ low = min(self.entry_temperature, self.exit_temperature)
1328
+ high = max(self.entry_temperature, self.exit_temperature)
1329
+ return low, high
1330
+
1331
+ def temperature_grid(
1332
+ self,
1333
+ lower: float | None = None,
1334
+ upper: float | None = None,
1335
+ min_points: int = 50,
1336
+ ) -> np.ndarray:
1337
+ """Create a temperature grid within the active interval for evaluation."""
1338
+ low, high = self.bounds()
1339
+ if lower is not None:
1340
+ low = max(low, lower)
1341
+ if upper is not None:
1342
+ high = min(high, upper)
1343
+ if high <= low:
1344
+ return np.array([])
1345
+
1346
+ raw = self.phase.T
1347
+ mask = (raw >= low) & (raw <= high)
1348
+ temps = raw[mask] # temperatures already in the phase trace
1349
+ if temps.size >= 2: # enough points already
1350
+ temps = np.concatenate((temps, [low, high]))
1351
+ temps = np.unique(temps)
1352
+ else:
1353
+ temps = np.linspace(low, high, min_points)
1354
+ return temps
1355
+
1356
+ def __call__(self, T: float | np.ndarray, deriv: int = 0) -> np.ndarray:
1357
+ """Evaluate the phase minimum at temperature ``T``."""
1358
+ return self.phase.valAt(T, deriv=deriv)
1359
+
1360
+
1361
+ class TransitionHistory:
1362
+ """Ordered collection of ``PhaseSegment`` objects describing the evolution."""
1363
+
1364
+ def __init__(self, segments: Iterable[PhaseSegment]):
1365
+ """Store the ordered list of phase segments traversed during cooling."""
1366
+ self._segments = list(segments)
1367
+
1368
+ def __len__(self) -> int:
1369
+ """Return the number of stored phase segments."""
1370
+ return len(self._segments)
1371
+
1372
+ def __iter__(self):
1373
+ """Iterate over the ordered phase segments."""
1374
+ return iter(self._segments)
1375
+
1376
+ def __getitem__(self, index: int) -> PhaseSegment:
1377
+ """Return the phase segment at ``index``."""
1378
+ return self._segments[index]
1379
+
1380
+ def keys(self):
1381
+ """Expose indices for iteration, matching list semantics."""
1382
+ return range(len(self._segments))
1383
+
1384
+ def items(self):
1385
+ """Yield ``(index, PhaseSegment)`` pairs in order."""
1386
+ for idx, segment in enumerate(self._segments):
1387
+ yield idx, segment
1388
+
1389
+ def values(self):
1390
+ """Return the ordered list of segments."""
1391
+ return list(self._segments)
1392
+
1393
+ def phase_keys(self) -> list[int | str]:
1394
+ """Return the ordered phase identifiers along the history."""
1395
+ return [segment.phase_key for segment in self._segments]
1396
+
1397
+ @property
1398
+ def temperature_span(self) -> tuple[float, float]:
1399
+ """Return the overall minimum and maximum temperature that are covered."""
1400
+ if not self._segments:
1401
+ return (0.0, 0.0)
1402
+ lows, highs = zip(*(segment.bounds() for segment in self._segments))
1403
+ return (min(lows), max(highs))
1404
+
1405
+
1406
+ def reconstructTransitionHistory(phases: "Phases",
1407
+ transitions: Iterable["TransitionInfo"]) -> TransitionHistory:
1408
+ """Construct the ordered thermal history using the traced phase graph.
1409
+
1410
+ Parameters
1411
+ ----------
1412
+ phases : Phases
1413
+ The traced phases together with their transition graph.
1414
+ transitions : Iterable[TransitionInfo]
1415
+ The ordered list of realised transitions starting from the high-T phase.
1416
+
1417
+ Returns
1418
+ -------
1419
+ TransitionHistory
1420
+ An ordered, piecewise representation of the vacuum expectation values
1421
+ that can be evaluated via the individual :class:`PhaseSegment` objects.
1422
+ """
1423
+ if phases is None:
1424
+ raise ValueError("No phases supplied to reconstructTransitionHistory.")
1425
+
1426
+ transition_list = list(transitions)
1427
+
1428
+ if not hasattr(phases, "rootPhase"):
1429
+ msg = "reconstructTransitionHistory now expects a Phases instance with a built phase graph."
1430
+ raise AttributeError(msg)
1431
+
1432
+ if len(transition_list) == 0:
1433
+ root_key = phases.rootPhase
1434
+ root_phase = phases[root_key]
1435
+ segment = PhaseSegment(
1436
+ index=0,
1437
+ phase_key=root_phase.key,
1438
+ phase=root_phase,
1439
+ entry_temperature=float(root_phase.Tmax),
1440
+ exit_temperature=float(root_phase.Tmin),
1441
+ )
1442
+ return TransitionHistory([segment])
1443
+
1444
+ start_key = phases.rootPhase
1445
+ if transition_list[0].high_phase != start_key:
1446
+ msg = (
1447
+ "Transition list does not start at the high-temperature phase. "
1448
+ f"Expected high phase {start_key}, received {transition_list[0].high_phase}."
1449
+ )
1450
+ raise ValueError(msg)
1451
+
1452
+ phase_keys: list[int | str] = [start_key]
1453
+ current_key = start_key
1454
+ for tr in transition_list:
1455
+ if tr.high_phase != current_key:
1456
+ msg = (
1457
+ "Inconsistent transition ordering: "
1458
+ f"expected high phase {current_key}, received {tr.high_phase}."
1459
+ )
1460
+ raise ValueError(msg)
1461
+ phase_keys.append(tr.low_phase)
1462
+ current_key = tr.low_phase
1463
+
1464
+ def _entry_temperature(tr: "TransitionInfo") -> float:
1465
+ if getattr(tr, "type", None) == 2:
1466
+ return float(tr.Tcrit)
1467
+ return float(tr.derived_params.get("Treh", tr.Tcrit))
1468
+
1469
+ def _exit_temperature(tr: "TransitionInfo") -> float:
1470
+ if getattr(tr, "type", None) == 2:
1471
+ return float(tr.Tcrit)
1472
+ return float(tr.derived_params.get("Tperc", tr.Tcrit))
1473
+
1474
+ segments: list[PhaseSegment] = []
1475
+ for idx, phase_key in enumerate(phase_keys):
1476
+ phase = phases[phase_key]
1477
+ if idx == 0:
1478
+ entry_temp = float(phase.Tmax)
1479
+ else:
1480
+ entry_temp = _entry_temperature(transition_list[idx - 1])
1481
+ if idx == len(phase_keys) - 1:
1482
+ exit_temp = float(phase.Tmin)
1483
+ else:
1484
+ exit_temp = _exit_temperature(transition_list[idx])
1485
+
1486
+ entry_temp = float(np.clip(entry_temp, phase.Tmin, phase.Tmax))
1487
+ exit_temp = float(np.clip(exit_temp, phase.Tmin, phase.Tmax))
1488
+
1489
+ segments.append(
1490
+ PhaseSegment(
1491
+ index=idx,
1492
+ phase_key=phase.key,
1493
+ phase=phase,
1494
+ entry_temperature=entry_temp,
1495
+ exit_temperature=exit_temp,
1496
+ )
1497
+ )
1498
+
1499
+ return TransitionHistory(segments)