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