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,1462 @@
1
+ """
2
+ A module for finding instantons between vacua in multiple field dimensions.
3
+
4
+ The basic strategy is an iterative process:
5
+
6
+ 1. Make an ansatz for the path along which the field will travel.
7
+ 2. Split up the equations of motion into components that are parallel and
8
+ perpendicular to the direction of travel along the path.
9
+ 3. The direction of motion parallel to the path reduces to a
10
+ one-dimensional equation of motion, which can be solved using the
11
+ overshoot / undershoot techniques in :mod:`.tunneling1D`. Solve it.
12
+ 4. Treating the motion of the field as a classical particle moving in an
13
+ inverted potential, calculate the normal forces that would need to act
14
+ on the particle to keep it on the path. If this forces are (close enough
15
+ to) zero, the ansatz was correctly. Otherwise iteratively deform the path
16
+ in the direction of the normal forces, stopping when the forces go to
17
+ zero.
18
+ 5. Loop back to step 3 until no further deformation is necessary.
19
+
20
+ The classes :class:`Deformation_Spline` and :class:`Deformation_Points` will
21
+ perform step 3, while :func:`fullTunneling` will run the entire loop.
22
+
23
+ For more explicit details, see the original paper
24
+ `Comput. Phys. Commun. 183 (2012)`_ [`arXiv:1109.4189`_].
25
+
26
+ .. _`Comput. Phys. Commun. 183 (2012)`:
27
+ http://dx.doi.org/10.1016/j.cpc.2012.04.004
28
+
29
+ .. _`arXiv:1109.4189`: http://arxiv.org/abs/1109.4189
30
+
31
+ Part of TransitionListener v2.0
32
+ Documentation: https://tasillo.de/TransitionListener/
33
+
34
+ Authors:
35
+ Jonas Matuszak <jonas.matuszak@kit.edu>
36
+ Carlo Tasillo <carlo.tasillo@ific.uv.es>
37
+ """
38
+
39
+ from typing import Callable
40
+ import numpy as np
41
+ from dataclasses import dataclass
42
+ from scipy import optimize, interpolate, integrate
43
+ from collections import namedtuple
44
+ from . import tunneling1D
45
+ from . import helper_functions
46
+ from . import errors
47
+
48
+ class Deformation_Spline(object):
49
+ """
50
+ Deform a path in the presence of a potential such that the normal forces
51
+ along the path are zero.
52
+
53
+ This class fits a spline to the points, and does the actual deformation
54
+ on the spline rather than on the points themselves. This make the path
55
+ somewhat smoother than it would otherwise be (which is generally desirable),
56
+ but it does make it difficult to resolve sharp turns in the path.
57
+
58
+ Parameters
59
+ ----------
60
+ phi : array_like
61
+ The list of points that constitutes the original path. Should have
62
+ shape ``(n_points, n_dimensions)``.
63
+ dphidr : array_like
64
+ The 'speed' along the path at the initial points. This does not change
65
+ as the path deforms. Should have shape ``(n_points,)``. Gets saved into
66
+ the attribute `v2` as ``v2 = dphidr[:,np.newaxis]**2``.
67
+ dV : callable
68
+ The potential gradient as a function of phi. The output shape should be
69
+ the same as the input shape, which will be ``(..., n_dimensions)``.
70
+ nb : int, optional
71
+ Number of basis splines to use.
72
+ kb : int, optional
73
+ Order of basis splines.
74
+ v2min : float, optional
75
+ The smallest the square of dphidr is allowed to be, relative
76
+ to the characteristic force exterted by F_ext. Note that the
77
+ self-correcting nature of the deformation goes away when dphidr=0.
78
+ fix_start, fix_end : bool, optional
79
+ If True, the force on the first/last point along the path is set to
80
+ zero, so the point will not change in the deformation step.
81
+ save_all_steps : bool, optional
82
+ If True, each step gets saved into ``self.phi_list`` and
83
+ ``self.F_list``.
84
+
85
+ Attributes
86
+ ----------
87
+ phi : array_like
88
+ Set during initialization, and then rewritten at each step.
89
+ num_steps : int
90
+ Total number of steps taken.
91
+ """
92
+ """
93
+ Additional (private) attributes
94
+ -------------------------------
95
+ _L : float
96
+ Total length of the path, set during initialization.
97
+ _t : array_like
98
+ Array from (0,1] marking the locations of each point.
99
+ _X, _dX, _d2X : array_like
100
+ Spline basis functions and their derivatives evaluated at `_t`. Set
101
+ during initialization.
102
+ _beta : array_like
103
+ The spline coefficients for each dimension. Recalculated each step.
104
+ _F_prev, _phi_prev : array_like
105
+ The normal force and the path points at the last step.
106
+ """
107
+ def __init__(self, phi: np.ndarray, dphidr: np.ndarray, dV: callable,
108
+ nb: int=10, kb: int=3, v2min: float=0.0,
109
+ fix_start: bool=False, fix_end: bool=False,
110
+ save_all_steps: bool=False):
111
+ """Pre-compute spline basis functions and store the deformation state."""
112
+ # First step: convert phi to a set of path lengths.
113
+ phi = np.asanyarray(phi)
114
+ dphi = phi[1:]-phi[:-1]
115
+ dL = np.sqrt(np.sum(dphi*dphi,axis=-1))
116
+ y = np.cumsum(dL)
117
+ self._L = y[-1]
118
+ self._t = np.append(0,y)/self._L
119
+ self._t[0] = 1e-100 # Without this, the first data point isn't in
120
+ # any bin (this matters for dX).
121
+
122
+ # Create the starting spline:
123
+ # make the knots and then the spline matrices at each point t
124
+ t0 = np.append(np.append([0.]*(kb-1), np.linspace(0,1,nb+3-kb)),
125
+ [1.]*(kb-1))
126
+ self._X,self._dX,self._d2X = helper_functions.Nbspld2(t0, self._t, kb)
127
+ self._t = self._t[:,np.newaxis] # Shape (n, 1)
128
+ # subtract off the linear component.
129
+ phi0, phi1 = phi[:1], phi[-1:] # These are shape (1,N)
130
+ phi_lin = phi0 + (phi1-phi0)*self._t
131
+ self._beta, residues, rank, s = np.linalg.lstsq(self._X, phi - phi_lin, rcond=None)
132
+
133
+ # save the points for future use.
134
+ self.phi = np.asanyarray(phi) # shape (n,N)
135
+ self.v2 = np.asanyarray(dphidr)[:,np.newaxis]**2 # shape (n,1)
136
+ self.dV = dV
137
+ self.F_list = []
138
+ self.phi_list = []
139
+ self._phi_prev = self._F_prev = None
140
+ self.save_all_steps = save_all_steps
141
+ self.fix_start, self.fix_end = fix_start, fix_end
142
+ self.num_steps = 0
143
+
144
+ # ensure that v2 isn't too small:
145
+ v2 = dphidr**2
146
+ v2min *= np.max(np.sum(dV(self.phi)**2, -1)**.5*self._L/nb)
147
+ v2[v2 < v2min] = v2min
148
+ self.v2 = v2[:,np.newaxis]
149
+
150
+ _forces_rval = namedtuple("forces_rval", "F_norm dV")
151
+ def forces(self):
152
+ """
153
+ Calculate the normal force and potential gradient on the path.
154
+
155
+ Returns
156
+ -------
157
+ F_norm, dV : array_like
158
+ """
159
+ X, dX, d2X = self._X, self._dX, self._d2X
160
+ beta = self._beta
161
+ # First find phi, dphi, and d2phi. Note that dphi needs to get a
162
+ # linear component added in, while d2phi does not.
163
+ phi = self.phi
164
+ dphi = np.sum(beta[np.newaxis,:,:]*dX[:,:,np.newaxis], axis=1) \
165
+ + (self.phi[-1]-self.phi[1])[np.newaxis,:]
166
+ d2phi = np.sum(beta[np.newaxis,:,:]*d2X[:,:,np.newaxis], axis=1)
167
+ # Compute dphi/ds, where s is the path length instead of the path
168
+ # parameter t. This is just the direction along the path.
169
+ dphi_sq = np.sum(dphi*dphi, axis=-1)[:,np.newaxis]
170
+ dphids = dphi/np.sqrt(dphi_sq)
171
+ # Then find the acceleration along the path, i.e. d2phi/ds2:
172
+ d2phids2 = (d2phi - dphi * np.sum(dphi*d2phi, axis=-1)[:,np.newaxis] /
173
+ dphi_sq)/dphi_sq
174
+ # Now we have the path at the points t, as well as its derivatives with
175
+ # respect to its path length. We still need the normal force on the path.
176
+ dV = self.dV(phi)
177
+ dV_perp = dV - np.sum(dV*dphids, axis=-1)[:,np.newaxis]*dphids
178
+ F_norm = d2phids2 * self.v2 - dV_perp
179
+ if (self.fix_start):
180
+ F_norm[0] = 0.0
181
+ if (self.fix_end):
182
+ F_norm[-1] = 0.0
183
+ return self._forces_rval(F_norm, dV)
184
+
185
+ _step_rval = namedtuple("step_rval", "stepsize step_reversed fRatio")
186
+ def step(self, lastStep: float, maxstep: float=.1, minstep: float=1e-4,
187
+ reverseCheck: float=.15, stepIncrease: float=1.5,
188
+ stepDecrease: float=5., checkAfterFit: bool=True,
189
+ verbose: bool=False):
190
+ """
191
+ Deform the path one step.
192
+
193
+ Each point is pushed in the direction of the normal force - the force
194
+ that the path exerts on a classical particle moving with speed `dphidr`
195
+ in a potential with gradient `dV` such that the particle stays on the
196
+ path. A stepsize of 1 corresponds to moving the path an amount
197
+ ``L*N/(dV_max)``, where `L` is the length of the (original) path,
198
+ `N` is the normal force, and `dV_max` is the maximum force exerted by
199
+ the potential along the path.
200
+
201
+ Parameters
202
+ ----------
203
+ lastStep : float
204
+ Size of the last step.
205
+ maxstep, minstep : float, optional
206
+ reverseCheck : float, optional
207
+ Percentage of points for which the force can reverse direcitons
208
+ (relative to the last step) before the stepsize is decreased.
209
+ If ``reverseCheck >= 1``, the stepsize is kept at `lastStep`.
210
+ stepIncrease, stepDecrease : float, optional
211
+ The amount to increase or decrease stepsize over the last step.
212
+ Both should be bigger than 1.
213
+ checkAfterFit : bool, optional
214
+ If True, the convergence test is performed after the points are fit
215
+ to a spline. If False, it's done beforehand.
216
+ verbose : bool, optional
217
+ If True, output is printed at each step.
218
+
219
+ Returns
220
+ -------
221
+ stepsize : float
222
+ The stepsize used for this step.
223
+ step_reversed : bool
224
+ True if this step was reversed, otherwise False
225
+ fRatio : float
226
+ The ratio of the maximum normal force to the maximum potential
227
+ gradient. When the path is a perfect fit, this should go to zero. If
228
+ ``checkAfterFit == True``, the normal force in this ratio is defined
229
+ by the change in phi this step *after* being fit to a spline. Note
230
+ that if the spline does a poor job of fitting the points after the
231
+ deformation in this step (which might be the case if there are not
232
+ enough basis functions), and if ``checkAfterFit == False``, this
233
+ ratio can be non-zero or large even if there is no change in ``phi``.
234
+
235
+ Notes
236
+ -----
237
+ In prior versions of this function (CosmoTransitions v1.0.2 and
238
+ earlier), the start and end points of the
239
+ path were effectively held fixed during the main deformation. This was
240
+ because the line ``phi_lin = phi[:1] + ...`` was calculated *before* the
241
+ line ``phi = phi+F*stepsize``. Since the spline basis functions are
242
+ zero at the start and end points (the spline is added on top of the
243
+ straight line between the end points), when the points were later taken
244
+ from the spline the end points wouldn't move. This was by design, since
245
+ for thin-walled bubbles the endpoints should stay fixed at the two
246
+ vacua. However, this caused problems for thick-walled bubbles where the
247
+ end points should move.
248
+
249
+ To get around this, prior versions added an extra block of code to move
250
+ the end points before the main deformation. However, this was
251
+ unnecessarily complicated and led to error-prone code. In this version,
252
+ the end points are always allowed to move if the force `F` is non-zero.
253
+ In the thin-walled case, the force should be almost exactly zero at
254
+ the end points anyways (there is zero potential gradient and `dphidr` is
255
+ zero), so they should stay fixed on their own.
256
+ """
257
+ # Find out the direction of the deformation.
258
+ F, dV = self.forces()
259
+ F_max = np.max(np.sqrt(np.sum(F*F,-1)))
260
+ dV_max = np.max(np.sqrt(np.sum(dV*dV,-1)))
261
+ fRatio1 = F_max / dV_max
262
+ # Rescale the normal force so that it's relative to L:
263
+ F *= self._L / dV_max
264
+
265
+ # Now, see how big the stepsize should be
266
+ stepsize = lastStep
267
+ phi = self.phi
268
+ assert(maxstep > minstep)
269
+ step_reversed = False
270
+ if reverseCheck < 1 and self._F_prev is not None:
271
+ FdotFlast = np.sum(F*self._F_prev, axis=1)
272
+ if np.sum(FdotFlast < 0) > len(FdotFlast)*reverseCheck:
273
+ # we want to reverse the last step
274
+ if stepsize > minstep:
275
+ step_reversed = True
276
+ phi = self._phi_prev
277
+ F = self._F_prev
278
+ if verbose: print("step reversed")
279
+ stepsize = lastStep/stepDecrease
280
+ else:
281
+ # No (large number of) indices reversed, just do a regular
282
+ # step. Increase the stepsize a bit over the last one.
283
+ stepsize = lastStep * stepIncrease
284
+ if stepsize > maxstep: stepsize = maxstep
285
+ if stepsize < minstep: stepsize = minstep
286
+
287
+ # Save the state before the step
288
+ self._phi_prev = phi
289
+ self._F_prev = F
290
+ if self.save_all_steps:
291
+ self.phi_list.append(phi)
292
+ self.F_list.append(F)
293
+
294
+ # Now make the step. It's important to not use += so we do not mutate
295
+ # the value stored in self.phi_list.
296
+ phi = phi+F*stepsize
297
+
298
+ # fit to the spline
299
+ phi_lin = phi[:1] + (phi[-1:]-phi[:1])*self._t
300
+ phi -= phi_lin
301
+ self._beta, residues, rank, s = np.linalg.lstsq(self._X, phi, rcond=None)
302
+ phi = np.sum(self._beta[np.newaxis,:,:]*self._X[:,:,np.newaxis], axis=1)
303
+ phi += phi_lin
304
+ self.phi = phi
305
+
306
+ Ffit = (phi-self._phi_prev)/stepsize
307
+ fRatio2 = np.max(np.sqrt(np.sum(Ffit*Ffit,-1)))/self._L
308
+
309
+ if verbose:
310
+ print("step: %i; stepsize: %0.2e; fRatio1 %0.2e; fRatio2: %0.2e"
311
+ % (self.num_steps, stepsize, fRatio1, fRatio2))
312
+
313
+ fRatio = fRatio2 if checkAfterFit else fRatio1
314
+ return self._step_rval(stepsize, step_reversed, fRatio)
315
+
316
+ def deformPath(self, startstep : float=2e-3, fRatioConv: float=.02,
317
+ converge_0: float=5., fRatioIncrease: float=5.,
318
+ maxiter: int=500, verbose: bool=False, callback=None,
319
+ step_params={}) -> bool:
320
+ """
321
+ Deform the path many individual steps, stopping either when the
322
+ convergence criterium is reached, when the maximum number of iterations
323
+ is reached, or when the path appears to be running away from
324
+ convergence.
325
+
326
+ Parameters
327
+ ----------
328
+ startstep : float, optional
329
+ Starting stepsize used in :func:`step`.
330
+ fRatioConv : float, optional
331
+ The routine will stop when the maximum normal force on the path
332
+ divided by the maximum potential gradient is less than this.
333
+ converge_0 : float, optional
334
+ On the first step, use a different convergence criterion. Check if
335
+ ``fRatio < convergence_0 * fRatioConv``.
336
+ fRatioIncrease :float, optional
337
+ The maximum fractional amount that fRatio can increase before
338
+ raising an error.
339
+ maxiter : int, optional
340
+ Maximum number of steps to take (ignoring reversed steps).
341
+ verbose : bool, optional
342
+ If True, print the ending condition.
343
+ callback : callable, optional
344
+ Called after each step. Should accept an instance of this class as a
345
+ parameter, and return False if deformation should stop.
346
+ step_params : dict, optional
347
+ Parameters to pass to :func:`step`.
348
+
349
+ Returns
350
+ -------
351
+ deformation_converged : bool
352
+ True if the routine stopped because it converged (as determined by
353
+ `fRatioConv`), False otherwise.
354
+ """
355
+ minfRatio = np.inf
356
+ minfRatio_index = 0
357
+ minfRatio_beta = None
358
+ minfRatio_phi = None
359
+ stepsize = startstep
360
+ deformation_converged = False
361
+ while True:
362
+ self.num_steps += 1
363
+ stepsize, step_reversed, fRatio = self.step(stepsize, **step_params)
364
+ if callback is not None and not callback(self):
365
+ break
366
+ minfRatio = min(minfRatio, fRatio)
367
+ if fRatio < fRatioConv or (self.num_steps == 1
368
+ and fRatio < converge_0*fRatioConv):
369
+ if verbose:
370
+ print("Path deformation converged. " + "%i steps. fRatio = %0.5e" % (self.num_steps,fRatio))
371
+ deformation_converged = True
372
+ break
373
+ if minfRatio == fRatio:
374
+ minfRatio_beta = self._beta
375
+ minfRatio_index = self.num_steps
376
+ minfRatio_phi = self.phi
377
+ if fRatio > fRatioIncrease*minfRatio and not step_reversed:
378
+ self._beta = minfRatio_beta
379
+ self.phi = minfRatio_phi
380
+ self.phi_list = self.phi_list[:minfRatio_index]
381
+ self.F_list = self.F_list[:minfRatio_index]
382
+ raise errors.DeformationError()
383
+ if self.num_steps >= maxiter:
384
+ if verbose:
385
+ print("Maximum number of deformation iterations reached.")
386
+ break
387
+ return deformation_converged
388
+
389
+
390
+ class Deformation_Points(object):
391
+ """
392
+ Deform a path in the presence of a potential such that the normal forces
393
+ along the path are zero.
394
+
395
+ Unlike :class:`Deformation_Spline`, this class changes the points
396
+ themselves rather than fitting a spline to the points. It is a more
397
+ straightforward implementation, and when run with comparable inputs (i.e.,
398
+ the number of basis splines is about the same as the number of points), this
399
+ method tends to be somewhat faster. The individual stepsizes here change
400
+ with the total number of points, whereas in the spline implementation they
401
+ mostly depend on the number of basis functions. However, as long as the path
402
+ is fairly smooth, the total number of splines in that class can probably be
403
+ smaller than the total number of points in this class, so this class will
404
+ tend to be somewhat slower.
405
+
406
+ The two implementations should converge upon the same answer when the
407
+ number of points and basis functions get large.
408
+
409
+ Parameters
410
+ ----------
411
+ phi : array_like
412
+ The list of points that constitutes the original path. Should have
413
+ shape ``(n_points, n_dimensions)``.
414
+ dphidr : array_like
415
+ The 'speed' along the path at the initial points. This does not change
416
+ as the path deforms. Should have shape ``(n_points,)``. Gets saved into
417
+ the attribute ``self.v2`` as ``v2 = dphidr[:,np.newaxis]**2``.
418
+ dV : callable
419
+ The potential gradient as a function of phi. The output shape should be
420
+ the same as the input shape, which will be ``(..., n_dimensions)``.
421
+ fix_start, fix_end : bool, optional
422
+ If True, the force on the first/last point along the path is set to
423
+ zero, so the point will not change in the deformation step.
424
+ save_all_steps : bool, optional
425
+ If True, each step gets saved into ``self.phi_list`` and
426
+ ``self.F_list``.
427
+
428
+ Attributes
429
+ ----------
430
+ phi : array_like
431
+ Set during initialization, and then rewritten at each step.
432
+ num_steps : int
433
+ Total number of steps taken.
434
+ """
435
+ def __init__(self, phi: np.ndarray, dphidr: np.ndarray,
436
+ dV: Callable[[np.ndarray], np.ndarray],
437
+ fix_start=False, fix_end=False, save_all_steps=False):
438
+ """Store the points and derivative data required for
439
+ point-wise deformation."""
440
+ self.phi = np.asanyarray(phi) # shape (n,N)
441
+ self.v2 = np.asanyarray(dphidr)[:,np.newaxis]**2 # shape (n,1)
442
+ self.dV = dV
443
+ self.F_list = []
444
+ self.phi_list = []
445
+ self.save_all_steps = save_all_steps
446
+ self.fix_start, self.fix_end = fix_start, fix_end
447
+ self.num_steps = 0
448
+
449
+ _forces_rval = namedtuple("forces_rval", "F_norm dV")
450
+ def forces(self, phi : np.ndarray=None):
451
+ """
452
+ Calculate the normal force and potential gradient on the path.
453
+
454
+ Returns
455
+ -------
456
+ F_norm, dV : array_like
457
+ """
458
+ if phi is None: phi = self.phi
459
+ # Let `t` be some variable that parametrizes the points such that
460
+ # t_i = i. Calculate the derivs of phi w/ respect to t.
461
+ dphi = helper_functions.deriv14_const_dx(phi.T).T
462
+ d2phi = helper_functions.deriv23_const_dx(phi.T).T
463
+ # Let `x` be some variable that parametrizes the path such that
464
+ # |dphi/dx| = 1. Calculate the derivs.
465
+ dphi_abssq = np.sum(dphi*dphi, axis=-1)[:,np.newaxis]
466
+ dphi /= np.sqrt(dphi_abssq) # This is now dphi/dx
467
+ d2phi /= dphi_abssq # = d2phi/dx2 + (dphi/dx)(d2phi/dt2)/(dphi/dt)^2
468
+ d2phi -= np.sum(d2phi*dphi, axis=-1)[:,np.newaxis] * dphi # = d2phi/dx2
469
+ # Calculate the total force.
470
+ dV = self.dV(phi)
471
+ dV_perp = dV - np.sum(dV*dphi, axis=-1)[:,np.newaxis] * dphi
472
+ F_norm = d2phi*self.v2 - dV_perp
473
+ if (self.fix_start):
474
+ F_norm[0] = 0.0
475
+ if (self.fix_end):
476
+ F_norm[-1] = 0.0
477
+ return self._forces_rval(F_norm, dV)
478
+
479
+ _step_rval = namedtuple("step_rval", "stepsize fRatio")
480
+ def step(self, stepsize: float, minstep: float,
481
+ diff_check: float=0.1, step_decrease: float=2.):
482
+ """
483
+ Take two half-steps in the direction of the normal force.
484
+
485
+ Parameters
486
+ ----------
487
+ stepsize : float
488
+ Determines change in ``phi``: ``phi += F_norm*stepsize``.
489
+ minstep : float
490
+ The smallest the stepsize is allowed to be.
491
+ diff_check : float, optional
492
+ The stepsize is chosen such that difference between the forces at
493
+ beginning of the step and halfway through the step is small
494
+ compared to the force itself: ``max(F2-F1) < diff_check * max(F1)``,
495
+ where ``max`` here really means the maximum absolute value of the
496
+ force in each direction.
497
+ step_decrease : float, optional
498
+ Amount by which to decrease the stepsize if the step is too big.
499
+
500
+ Returns
501
+ -------
502
+ stepsize : float
503
+ The stepsize used for this step.
504
+ fRatio : float
505
+ The ratio of the maximum normal force to the maximum potential
506
+ gradient. When the path is a perfect fit, this should go to zero.
507
+ """
508
+ F1, dV = self.forces()
509
+ F_max = np.max(np.sqrt(np.sum(F1*F1,-1)))
510
+ dV_max = np.max(np.sqrt(np.sum(dV*dV,-1)))
511
+ fRatio = F_max / dV_max
512
+
513
+ if self.save_all_steps:
514
+ self.phi_list.append(self.phi)
515
+ self.F_list.append(F1)
516
+
517
+ while True:
518
+ # Take one full step
519
+ # phi1 = self.phi + F*stepsize
520
+ # Take two half steps
521
+ phi2 = self.phi + F1*(stepsize*0.5)
522
+ F2 = self.forces(phi2)[0]
523
+ if stepsize <= minstep:
524
+ stepsize = minstep
525
+ break
526
+ # phi2 += F2*(stepsize*0.5)
527
+ DF_max = np.max(np.abs(F2-F1), axis=0)
528
+ F_max = np.max(np.abs(F1), axis=0)
529
+ if (DF_max < diff_check*F_max).all():
530
+ break
531
+ stepsize /= step_decrease
532
+ self.phi = phi2 + F2*(stepsize*0.5)
533
+
534
+ return self._step_rval(stepsize, fRatio)
535
+
536
+ def deformPath(self, startstep=.1, minstep=1e-6, step_increase=1.5,
537
+ fRatioConv=.02, converge_0=5., fRatioIncrease=20.,
538
+ maxiter=500, verbose=0, callback=None, step_params={}):
539
+ """
540
+ Deform the path many individual steps, stopping either when the
541
+ convergence criterium is reached, when the maximum number of iterations
542
+ is reached, or when the path appears to be running away from
543
+ convergence.
544
+
545
+ Parameters
546
+ ----------
547
+ startstep, maxstep : float, optional
548
+ Starting and maximum stepsizes used in :func:`step`, rescaled by
549
+ ``|phi[0]-phi[1]| / (max(dV)*num_points)``.
550
+ fRatioConv : float, optional
551
+ The routine will stop when the maximum normal force on the path
552
+ divided by the maximum potential gradient is less than this.
553
+ converge_0 : float, optional
554
+ On the first step, use a different convergence criterion. Check if
555
+ ``fRatio < convergence_0 * fRatioConv``.
556
+ fRatioIncrease :float, optional
557
+ The maximum fractional amount that fRatio can increase before
558
+ raising an error.
559
+ maxiter : int, optional
560
+ Maximum number of steps to take (ignoring reversed steps).
561
+ verbose : int, optional
562
+ If ``verbose >= 1``, print the ending condition.
563
+ If ``verbose >= 2``, print `fRatio` and `stepsize` at each step.
564
+ callback : callable, optional
565
+ Called after each step. Should accept an instance of this class as a
566
+ parameter, and return False if deformation should stop.
567
+ step_params : dict, optional
568
+ Parameters to pass to :func:`step`.
569
+
570
+ Returns
571
+ -------
572
+ deformation_converged : bool
573
+ True if the routine stopped because it converged (as determined by
574
+ `fRatioConv`), False otherwise.
575
+ """
576
+ minfRatio = np.inf
577
+ Delta_phi = np.sum(np.sqrt((self.phi[0]-self.phi[-1])**2))
578
+ dV_max = np.max(np.sum(self.dV(self.phi)**2, axis=-1))**0.5
579
+ step_scale = Delta_phi / (len(self.phi) * dV_max)
580
+ stepsize = startstep * step_scale
581
+ minstep *= step_scale
582
+ deformation_converged = False
583
+ while True:
584
+ self.num_steps += 1
585
+ stepsize, fRatio = self.step(stepsize, minstep, **step_params)
586
+ if verbose >= 2:
587
+ print("step: %i; stepsize: %0.2e; fRatio: %0.2e"
588
+ % (self.num_steps, stepsize, fRatio))
589
+ stepsize *= step_increase
590
+ if callback is not None and not callback(self):
591
+ break
592
+ minfRatio = min(minfRatio, fRatio)
593
+ if fRatio < fRatioConv or (self.num_steps == 1
594
+ and fRatio < converge_0*fRatioConv):
595
+ if verbose >= 1:
596
+ print("Path deformation converged." +
597
+ "%i steps. fRatio = %0.5e" % (self.num_steps,fRatio))
598
+ deformation_converged = True
599
+ break
600
+ if minfRatio == fRatio:
601
+ minfRatio_index = self.num_steps
602
+ minfRatio_phi = self.phi
603
+ if fRatio > fRatioIncrease*minfRatio:
604
+ self.phi = minfRatio_phi
605
+ self.phi_list = self.phi_list[:minfRatio_index]
606
+ self.F_list = self.F_list[:minfRatio_index]
607
+ raise errors.DeformationError()
608
+ if self.num_steps >= maxiter:
609
+ if verbose: print("Maximum number of iterations reached.")
610
+ break
611
+ return deformation_converged
612
+
613
+
614
+ _extrapolatePhi_rtype = namedtuple("extrapolatePhi_rval", "phi s L")
615
+ def _extrapolatePhi(phi0, V=None, tails=0.2):
616
+ """
617
+ Returns a list of points along the path, going linearly
618
+ beyond the path to include the nearest minima.
619
+
620
+ Parameters
621
+ ----------
622
+ phi0 : array_like
623
+ The (multi-dimensional) path to extend.
624
+ V : callable or None
625
+ The potential to minimize, or None if the path should be extended a
626
+ fixed amount beyond its ends.
627
+ tails : float
628
+ The amount relative to the path length to extrapolate beyond the end of
629
+ the path (if V is None) or beyond the minima (if V is not None).
630
+
631
+ Returns
632
+ -------
633
+ phi : array_like
634
+ The extended list of points. The spacing between points in the extended
635
+ regions should be approximately the same as the spacing between the
636
+ input points.
637
+ s : array_like
638
+ The distance along the path (starting at ``phi0[0]``).
639
+ L : float
640
+ Total length of the path excluding tails.
641
+ """
642
+ phi1 = phi = phi0
643
+ dphi = np.append(0,np.sum((phi1[1:]-phi1[:-1])**2,1)**.5)
644
+ s1 = np.cumsum(dphi)
645
+ L = s1[-1]
646
+ npoints = phi1.shape[0]
647
+
648
+ phi_hat0 = (phi[1]-phi[0])/np.sum((phi[1]-phi[0])**2)**.5
649
+ if V is None:
650
+ s0min = 0.0
651
+ else:
652
+ V0 = lambda x: V(phi[0] + phi_hat0*x*L)
653
+ s0min = optimize.fmin(V0, 0.0, disp=0, xtol=1e-5)[0]*L
654
+ if s0min > 0: s0min = 0.0
655
+ s0 = np.linspace(s0min - L*tails, 0.0, npoints*tails)[:-1]
656
+ phi0 = phi[0] + phi_hat0*s0[:,np.newaxis]
657
+
658
+ phi_hat2 = (phi[-1]-phi[-2])/np.sum((phi[-1]-phi[-2])**2)**.5
659
+ if V is None:
660
+ s2min = 0.0
661
+ else:
662
+ V2 = lambda x: V(phi[-1] + phi_hat2*(x-1)*L)
663
+ s2min = optimize.fmin(V2, 1, disp=0, xtol=1e-5)[0]*L
664
+ if s2min < L: s2min = L
665
+ s2 = np.linspace(L, s2min + L*tails, npoints*tails)[1:]
666
+ phi2 = phi[-1] + phi_hat2*(s2[:,np.newaxis]-L)
667
+
668
+ phi = np.append(phi0, np.append(phi1, phi2, 0), 0)
669
+ s = np.append(s0, np.append(s1, s2))
670
+
671
+ return _extrapolatePhi_rtype(phi, s, L)
672
+
673
+
674
+ def _pathDeriv(phi):
675
+ """Calculates to 4th order if len(phi) >= 5, otherwise 1st/2nd order."""
676
+ if len(phi) >= 5:
677
+ dphi = helper_functions.deriv14_const_dx(phi.T).T
678
+ elif len(phi) > 2:
679
+ dphi = np.empty_like(phi)
680
+ dphi[1:-1] = 0.5*(phi[2:] - phi[:-2])
681
+ dphi[0] = -1.5*phi[0] + 2*phi[1] - 0.5*phi[2]
682
+ dphi[-1] = +1.5*phi[-1] - 2*phi[-2] + 0.5*phi[-3]
683
+ else:
684
+ dphi = np.empty_like(phi)
685
+ dphi[:] = phi[1]-phi[0]
686
+ return dphi
687
+
688
+
689
+ class SplinePath(object):
690
+ """
691
+ Fit a spline to a path in field space, and find the potential on that path.
692
+
693
+ The spline-fitting happens in several steps:
694
+
695
+ 1. The derivatives of the input points are found, and used to
696
+ determine the path length and direction at each point.
697
+ 2. If `extend_to_minima` is True, additional points are added at each end
698
+ of the path such that ends lie on local minima.
699
+ 3. The points are fit to a spline, with the knots given by the path
700
+ distances from the first point.
701
+ 4. If `reeval_distances` is True, the distances to each point are
702
+ re-evaluated using the spline. A new spline is fit with more accurate
703
+ knots.
704
+
705
+ The potential as a function of distance can be defined in one of two ways.
706
+ If `V_spline_samples` is None, the potential as a function of distance `x`
707
+ along the path is given by `V[pts(x)]`, where `pts(x)` is the spline
708
+ function that defines the path. If `V_spline_samples` is not None, the
709
+ potential is first evaluated `V_spline_samples` times along the path, and
710
+ another spline is fit to the output. In other words, when `V_spline_samples`
711
+ is None, the input potential `V` is evaluated for every value `x` passed to
712
+ to the class method :meth:`V`, whereas if `V_spline_samples` is not None,
713
+ the input potential is only evaluated during initialization.
714
+
715
+ Parameters
716
+ ----------
717
+ pts : array_like
718
+ The points that describe the path, with shape ``(num_points, N_dim)``.
719
+ V : callable
720
+ The potential function. Input arrays will be shape ``(npts, N_dim)`` and
721
+ output should have shape ``(npts,)``. Can be None.
722
+ dV : callable, optional.
723
+ The gradient of the potential. Input arrays will be shape
724
+ ``(npts, N_dim)`` and output should have shape ``(npts, N_dim)``. Only
725
+ used if ``V_spline_samples=None``.
726
+ V_spline_samples : int or None, optional
727
+ Number of samples to take along the path to create the spline
728
+ interpolation functions. If None, the potential is evaluated directly
729
+ from `V` given in the input. If not None, `V_spline_samples` should be
730
+ large enough to resolve the smallest features in the potential. For
731
+ example, the potential may have a very narrow potential barrier over
732
+ which multiple samples should be taken.
733
+ extend_to_minima : bool, optional
734
+ If True, the input path is extended at each end until it hits local
735
+ minima.
736
+ reeval_distances : bool, optional
737
+ If True, get more accurate distances to each knot by integrating along
738
+ the spline.
739
+
740
+ Attributes
741
+ ----------
742
+ L : float
743
+ The total length of the path.
744
+ """
745
+ def __init__(self, pts: np.ndarray, V: Callable[[np.ndarray], np.ndarray],
746
+ dV: Callable[[np.ndarray], np.ndarray]=None, V_spline_samples: int=100,
747
+ extend_to_minima: bool=False, reeval_distances: bool=True,
748
+ verbose: bool=False):
749
+ """Construct a spline representation of the tunnelling path."""
750
+ assert len(pts) > 1
751
+ # 1. Find derivs
752
+ dpts = _pathDeriv(pts)
753
+ # 2. Extend the path
754
+ if extend_to_minima:
755
+ def V_lin(x, p0, dp0, V): return V(p0+x*dp0)
756
+ # extend at the front of the path
757
+ xmin = optimize.fmin(V_lin, 0.0, args=(pts[0], dpts[0], V),
758
+ xtol=1e-6, disp=0)[0] # Setting xtol to 1e-12 was okay, too
759
+ if xmin > 0.0: xmin = 0.0
760
+ nx = np.ceil(abs(xmin)-.5) + 1
761
+ if nx > 20000:
762
+ raise errors.SplineError(
763
+ "Maximum allowed size exceeded while extending the tunnelling path towards the minimum."
764
+ )
765
+ if nx > 100_000 and verbose:
766
+ msg = "WARNING: The tunneling path appears to be very long: Tried to extend it by %i points" % nx
767
+ msg += ", which is more than 100_000. This may lead to numerical issues."
768
+ msg += "\nThe point where this happens is at %s, with derivative = %s." % (pts[0], dpts[0])
769
+ print(msg)
770
+ x = np.linspace(xmin, 0, int(nx))[:, np.newaxis]
771
+ pt_ext = pts[0] + x*dpts[0]
772
+
773
+ # Sometimes there are unnecessary dimensions in the input. The
774
+ # combination of atleast_2d and squeeze removes those and avoids
775
+ # issues with the concatenation.
776
+ pt_ext = np.atleast_2d(np.squeeze(pt_ext))
777
+ pts_rest = np.atleast_2d(np.squeeze(pts[1:]))
778
+ pts = np.concatenate([pt_ext, pts_rest], axis=0)
779
+
780
+ # extend at the end of the path
781
+ xmin = optimize.fmin(V_lin, 0.0, args=(pts[-1], dpts[-1], V),
782
+ xtol=1e-6, disp=0)[0] # Setting xtol to 1e-12 was okay, too
783
+ if xmin < 0.0: xmin = 0.0
784
+ nx = np.ceil(abs(xmin)-.5) + 1
785
+ if nx > 20000:
786
+ raise errors.SplineError(
787
+ "Maximum allowed size exceeded while extending the tunnelling path towards the second minimum."
788
+ )
789
+ if nx > 100_000 and verbose:
790
+ msg = "WARNING: The tunneling path appears to be very long: Tried to extend it by %i points" % nx
791
+ msg += ", which is more than 100_000. This may lead to numerical issues."
792
+ msg += "\nThe point where this happens is at %s, with derivative = %s." % (pts[-1], dpts[-1])
793
+ print(msg)
794
+ x = np.linspace(xmin, 0, int(nx))[::-1, np.newaxis]
795
+ pt_ext = pts[-1] + x*dpts[-1]
796
+ pts = np.append(pts[:-1], pt_ext, axis=0)
797
+ # Recalculate the derivative
798
+ dpts = _pathDeriv(pts)
799
+ # 3. Find knot positions and fit the spline.
800
+ pdist = integrate.cumulative_trapezoid(np.sqrt(np.sum(dpts*dpts, axis=1)),
801
+ initial=0.0)
802
+ self.L = pdist[-1]
803
+ k = min(len(pts)-1, 3) # degree of the spline
804
+ self._path_tck = interpolate.splprep(pts.T, u=pdist, s=0, k=k)[0]
805
+ # 4. Re-evaluate the distance to each point.
806
+ if reeval_distances:
807
+ def dpdx(_, x):
808
+ dp = np.array(interpolate.splev(x, self._path_tck, der=1))
809
+ return np.sqrt(np.sum(dp*dp))
810
+ pdist = integrate.odeint(dpdx, 0., pdist,
811
+ rtol=0, atol=pdist[-1]*1e-8)[:,0]
812
+ self.L = pdist[-1]
813
+ self._path_tck = interpolate.splprep(pts.T, u=pdist, s=0, k=k)[0]
814
+ # Now make the potential spline.
815
+ self._V = V
816
+ self._dV = dV
817
+ self._V_tck = None
818
+ if V_spline_samples is not None:
819
+ x = np.linspace(0,self.L,V_spline_samples)
820
+ # extend 20% beyond this so that we more accurately model the
821
+ # path end points
822
+ x_ext = np.arange(x[1], self.L*.2, x[1])
823
+ x = np.append(-x_ext[::-1], x)
824
+ x = np.append(x, self.L+x_ext)
825
+ y = self.V(x)
826
+ self._V_tck = interpolate.splrep(x,y,s=0)
827
+
828
+ def V(self, x):
829
+ """The potential as a function of the distance `x` along the path."""
830
+ if self._V_tck is not None:
831
+ return interpolate.splev(x, self._V_tck, der=0)
832
+ else:
833
+ pts = interpolate.splev(x, self._path_tck)
834
+ return self._V(np.array(pts).T)
835
+
836
+ def dV(self, x):
837
+ """`dV/dx` as a function of the distance `x` along the path."""
838
+ if self._V_tck is not None:
839
+ return interpolate.splev(x, self._V_tck, der=1)
840
+ else:
841
+ pts = interpolate.splev(x, self._path_tck)
842
+ dpdx = interpolate.splev(x, self._path_tck, der=1)
843
+ dV = self._dV(np.array(pts).T)
844
+ return np.sum(dV.T*dpdx, axis=0)
845
+
846
+ def d2V(self, x):
847
+ """`d^2V/dx^2` as a function of the distance `x` along the path."""
848
+ if self._V_tck is not None:
849
+ return interpolate.splev(x, self._V_tck, der=2)
850
+ else:
851
+ raise RuntimeError("No spline specified. Cannot calculate d2V.")
852
+
853
+ def pts(self, x):
854
+ """
855
+ Returns the path points as a function of the distance `x` along the
856
+ path. Return value is an array with shape ``(len(x), N_dim)``.
857
+ """
858
+ pts = interpolate.splev(x, self._path_tck)
859
+ return np.array(pts).T
860
+
861
+
862
+ @dataclass
863
+ class TunnelingSettings:
864
+ """Configuration bundle passed around the full tunnelling workflow."""
865
+
866
+ V_spline_samples: int | None
867
+ tunneling_class: type
868
+ tunneling_init_params: dict
869
+ tunneling_findProfile_params: dict
870
+ deformation_class: type
871
+ deformation_init_params: dict
872
+ deformation_deform_params: dict
873
+ save_all_steps: bool
874
+ verbose: bool
875
+ callback: Callable | None
876
+ callback_data: object
877
+
878
+
879
+ @dataclass
880
+ class TunnelingOutputs:
881
+ """Intermediate data collected during a single deformation iteration."""
882
+
883
+ path: SplinePath
884
+ tobj: object
885
+ profile1D: object
886
+ phi: np.ndarray
887
+ dphi: np.ndarray
888
+ pts: np.ndarray
889
+ deform_obj: Deformation_Spline
890
+ converged: bool
891
+
892
+
893
+ def _build_tunneling_settings(
894
+ V_spline_samples,
895
+ tunneling_class,
896
+ tunneling_init_params,
897
+ tunneling_findProfile_params,
898
+ deformation_class,
899
+ deformation_init_params,
900
+ deformation_deform_params,
901
+ save_all_steps: bool,
902
+ verbose: bool,
903
+ callback,
904
+ callback_data,
905
+ ) -> TunnelingSettings:
906
+ """Collect parameters and defaults used throughout the tunnelling routine."""
907
+ return TunnelingSettings(
908
+ V_spline_samples=V_spline_samples,
909
+ tunneling_class=tunneling_class,
910
+ tunneling_init_params=dict(tunneling_init_params or {}),
911
+ tunneling_findProfile_params=dict(tunneling_findProfile_params or {}),
912
+ deformation_class=deformation_class,
913
+ deformation_init_params=dict(deformation_init_params or {}),
914
+ deformation_deform_params=dict(deformation_deform_params or {}),
915
+ save_all_steps=save_all_steps,
916
+ verbose=verbose,
917
+ callback=callback,
918
+ callback_data=callback_data,
919
+ )
920
+
921
+
922
+ def _fit_spline_path(pts, settings: TunnelingSettings, V, dV, verbose):
923
+ """Fit a spline to the current path guess; translate common errors."""
924
+ try:
925
+ return SplinePath(
926
+ pts,
927
+ V,
928
+ dV,
929
+ V_spline_samples=settings.V_spline_samples,
930
+ extend_to_minima=True,
931
+ verbose=verbose,
932
+ )
933
+ except ValueError as err:
934
+ if np.isnan(pts).any():
935
+ if verbose:
936
+ print("NaN in the path points. Returning InfiniteActionError.")
937
+ raise errors.InfiniteActionError from err
938
+ if verbose:
939
+ print(err.args[0])
940
+ raise errors.SplineError(err.args[0])
941
+ except Exception as err:
942
+ msg = err.args[0]
943
+ if msg == "Maximum allowed size exceeded":
944
+ msg = (
945
+ "Failed to fit a spline to the tunneling path. Tunneling "
946
+ "computation failed because the number of points to "
947
+ "construct a spline along the tunneling path is too large. "
948
+ "This error usually happens when the points between the "
949
+ "tunneling should appear don't match the points in the "
950
+ "potential. Make sure that phase.valAt(T) is only called "
951
+ "in the allowed range of temperatures."
952
+ )
953
+ if msg == "Invalid inputs.":
954
+ msg = (
955
+ "Invalid inputs to SplinePath. It might be that these are "
956
+ "close to each other because phitol in the tunneling "
957
+ "parameters was not set small enough: pts = %s"
958
+ ) % (pts,)
959
+ if np.isclose(pts[0], pts[-1]).all():
960
+ msg += (
961
+ " The first and last points are identical. Return a "
962
+ "PotentialError with error_type 'no barrier'."
963
+ )
964
+ if verbose:
965
+ print(msg)
966
+ raise errors.PotentialError(msg, "no barrier") from err
967
+ if verbose:
968
+ print(msg)
969
+ raise errors.SplineError(msg) from err
970
+
971
+
972
+ def _create_tunneling_object(path: SplinePath, settings: TunnelingSettings):
973
+ """Instantiate the 1D tunnelling solver for the current path."""
974
+ if settings.V_spline_samples is not None:
975
+ return settings.tunneling_class(
976
+ 0.0,
977
+ path.L,
978
+ path.V,
979
+ path.dV,
980
+ path.d2V,
981
+ **settings.tunneling_init_params,
982
+ )
983
+ return settings.tunneling_class(
984
+ 0.0,
985
+ path.L,
986
+ path.V,
987
+ path.dV,
988
+ None,
989
+ **settings.tunneling_init_params,
990
+ )
991
+
992
+
993
+ def _find_profile(tobj, settings: TunnelingSettings):
994
+ """Run the 1D tunnelling solver and normalise its exceptions."""
995
+ try:
996
+ return tobj.findProfile(**settings.tunneling_findProfile_params)
997
+ except errors.TooSmalldVError as err:
998
+ raise errors.InfiniteActionError from err
999
+ except errors.TunnelingError as err:
1000
+ if settings.verbose:
1001
+ print(err.args[0])
1002
+ raise errors.InfiniteActionError from err
1003
+
1004
+
1005
+ def _prepare_profile(tobj, profile1D):
1006
+ """Re-sample the instanton profile on an evenly spaced grid."""
1007
+ phi, dphi = profile1D.Phi, profile1D.dPhi
1008
+ phi, dphi = tobj.evenlySpacedPhi(phi, dphi, npoints=len(phi), fixAbs=False)
1009
+ dphi[0] = dphi[-1] = 0.0
1010
+ return phi, dphi
1011
+
1012
+
1013
+ def _deform_path(
1014
+ path: SplinePath,
1015
+ tobj,
1016
+ profile1D,
1017
+ phi,
1018
+ dphi,
1019
+ dV,
1020
+ pts,
1021
+ settings: TunnelingSettings,
1022
+ saved_steps,
1023
+ ) -> TunnelingOutputs:
1024
+ """Apply the transverse deformation step and track convergence state."""
1025
+ deform_obj = settings.deformation_class(pts, dphi, dV, **settings.deformation_init_params)
1026
+ if settings.callback and not settings.callback(path, tobj, profile1D, settings.callback_data):
1027
+ if not hasattr(deform_obj, "num_steps"):
1028
+ deform_obj.num_steps = 0
1029
+ return TunnelingOutputs(path, tobj, profile1D, phi, dphi, pts, deform_obj, True)
1030
+ try:
1031
+ converged = deform_obj.deformPath(**settings.deformation_deform_params)
1032
+ except errors.DeformationError as err:
1033
+ if settings.verbose:
1034
+ print(err.args[0])
1035
+ converged = False
1036
+ pts = deform_obj.phi
1037
+ if settings.save_all_steps:
1038
+ saved_steps.append(deform_obj.phi_list)
1039
+ return TunnelingOutputs(path, tobj, profile1D, phi, dphi, pts, deform_obj, converged)
1040
+
1041
+
1042
+ def _should_stop(iter_output: TunnelingOutputs) -> bool:
1043
+ """Check if the deformation converged after a single, small adjustment."""
1044
+ return iter_output.converged and iter_output.deform_obj.num_steps < 2
1045
+
1046
+
1047
+ def _compute_force_ratio(pts, dphi, dV, settings: TunnelingSettings) -> float:
1048
+ """Compute the ratio of transverse forces to the gradient along the path."""
1049
+ deform_obj = settings.deformation_class(pts, dphi, dV, **settings.deformation_init_params)
1050
+ F, dV_vec = deform_obj.forces()
1051
+ F_max = np.max(np.sqrt(np.sum(F * F, -1)))
1052
+ dV_max = np.max(np.sqrt(np.sum(dV_vec * dV_vec, -1)))
1053
+ return F_max / dV_max
1054
+
1055
+
1056
+ def fullTunneling(
1057
+ path_pts,
1058
+ V,
1059
+ dV,
1060
+ maxiter=20,
1061
+ save_all_steps=False,
1062
+ verbose=False,
1063
+ callback=None,
1064
+ callback_data=None,
1065
+ V_spline_samples=100,
1066
+ tunneling_class=tunneling1D.SingleFieldInstanton,
1067
+ tunneling_init_params=None,
1068
+ tunneling_findProfile_params=None,
1069
+ deformation_class=Deformation_Spline,
1070
+ deformation_init_params=None,
1071
+ deformation_deform_params=None,
1072
+ ):
1073
+ """
1074
+ Calculate the instanton solution in multiple field dimensions.
1075
+
1076
+ This function works by looping four steps:
1077
+
1078
+ 1. Fit a spline to the path given by `path_pts`.
1079
+ 2. Calculate the one-dimensional tunneling along this path.
1080
+ 3. Deform the path to satisfy the transverse equations of motion.
1081
+ 4. Check for convergence, and then go back to step 1.
1082
+
1083
+ Parameters
1084
+ ----------
1085
+ path_pts : array_like
1086
+ An array of points that constitute the initial guess for the tunneling
1087
+ path, with shape ``(num_points, N_dim)``. The first point should be at
1088
+ (or near) the lower minimum (the minimum to which the field is
1089
+ tunneling), and the last point should be at the metastable minimum.
1090
+ V, dV : callable
1091
+ The potential function and its gradient. Both should accept input of
1092
+ shape ``(num_points, N_dim)`` or ``(N_dim,)``.
1093
+ maxiter : int, optional
1094
+ Maximum number of allowed deformation / tunneling iterations.
1095
+ save_all_steps : bool, optional
1096
+ If True, additionally output every single deformation sub-step.
1097
+ verbose : bool, optional
1098
+ If True, print a message at the start of each step.
1099
+ callback : callable, optional
1100
+ User supplied function that is evaluated just prior to deforming the
1101
+ path. Should return True if the path should be deformed, and False if
1102
+ the deformation should be aborted. Should accept 4 arguments: a
1103
+ :class:`SplinePath` instance which describes the tunneling path, a
1104
+ tunneling object (instance of ``tunneling_class``), the profile found
1105
+ by the tunneling object, and extra callback data.
1106
+ callback_data : any type, optional
1107
+ Extra data to pass to the callback function if there is one.
1108
+
1109
+ Other Parameters
1110
+ ----------------
1111
+ V_spline_samples : int, optional
1112
+ Passed to :class:`SplinePath`. If None, no second derivative will be
1113
+ passed to the tunneling class, and it will instead be evaluated using
1114
+ finite differences.
1115
+ tunneling_class : class, optional
1116
+ Either :class:`tunneling1D.SingleFieldInstanton` or a subclass.
1117
+ tunneling_init_params : dict, optional
1118
+ Extra parameters to pass to the tunneling class.
1119
+ tunneling_findProfile_params : dict, optional
1120
+ Extra parameters to pass to ``tunneling_class.findProfile()``.
1121
+ deformation_class : class, optional
1122
+ Either :class:`Deformation_Spline` or :class:`Deformation_Points`, or
1123
+ some other object that exposes the same interface.
1124
+ deformation_init_params : dict, optional
1125
+ Extra parameters to pass to the deformation class.
1126
+ deformation_deform_params : dict, optional
1127
+ Extra parameters to pass to ``deformation_class.deformPath()``.
1128
+
1129
+ Returns
1130
+ -------
1131
+ profile1D : namedtuple
1132
+ The return value from ``tunneling_class.findProfile()``.
1133
+ Phi : array_like
1134
+ The points that constitute the final deformed path. They are in
1135
+ one-to-one correspondence with the points in `profile1D`.
1136
+ action : float
1137
+ The Euclidean action of the instanton.
1138
+ fRatio : float
1139
+ A ratio of the largest transverse force on the final path relative to
1140
+ the largest potential gradient. This would be zero if the solution were
1141
+ perfect.
1142
+ saved_steps : list
1143
+ A list of lists, with each sub-list containing the saved steps for each
1144
+ deformation. Only written to if `save_all_steps` is True.
1145
+ """
1146
+ settings = _build_tunneling_settings(
1147
+ V_spline_samples,
1148
+ tunneling_class,
1149
+ tunneling_init_params,
1150
+ tunneling_findProfile_params,
1151
+ deformation_class,
1152
+ deformation_init_params,
1153
+ deformation_deform_params,
1154
+ save_all_steps,
1155
+ verbose,
1156
+ callback,
1157
+ callback_data,
1158
+ )
1159
+ settings.deformation_init_params.setdefault("save_all_steps", save_all_steps)
1160
+ if settings.save_all_steps:
1161
+ settings.deformation_init_params["save_all_steps"] = True
1162
+
1163
+ pts = np.asanyarray(path_pts)
1164
+ saved_steps = []
1165
+ last_output: TunnelingOutputs | None = None
1166
+
1167
+ for _ in range(1, maxiter + 1):
1168
+ # 1. Fit the spline to the path.
1169
+ path = _fit_spline_path(pts, settings, V, dV, verbose)
1170
+ # 2. Instantiate the 1D tunnelling solver and find the profile.
1171
+ tobj = _create_tunneling_object(path, settings)
1172
+ profile1D = _find_profile(tobj, settings)
1173
+ phi, dphi = _prepare_profile(tobj, profile1D)
1174
+ # 3. Deform the path to satisfy the transverse equations of motion.
1175
+ path_points = path.pts(phi)
1176
+ iter_output = _deform_path(
1177
+ path,
1178
+ tobj,
1179
+ profile1D,
1180
+ phi,
1181
+ dphi,
1182
+ dV,
1183
+ path_points,
1184
+ settings,
1185
+ saved_steps,
1186
+ )
1187
+ pts = iter_output.pts
1188
+ last_output = iter_output
1189
+ # 4. Check whether the deformation has converged.
1190
+ if _should_stop(iter_output):
1191
+ break
1192
+ else:
1193
+ if settings.verbose:
1194
+ print("Reached maxiter in fullTunneling. No convergence.")
1195
+
1196
+ if last_output is None:
1197
+ raise errors.InfiniteActionError
1198
+
1199
+ f_ratio = _compute_force_ratio(last_output.pts, last_output.dphi, dV, settings)
1200
+ rtuple = namedtuple(
1201
+ "fullTunneling_rval", "profile1D Phi action fRatio saved_steps"
1202
+ )
1203
+ Phi = last_output.path.pts(last_output.profile1D.Phi)
1204
+ action = last_output.tobj.findAction(last_output.profile1D)
1205
+ return rtuple(last_output.profile1D, Phi, action, f_ratio, saved_steps)
1206
+
1207
+
1208
+ def _validate_tunneling_path(x0: np.ndarray, x1: np.ndarray, V_) -> None:
1209
+ """Sanity-check that the path spans distinct minima and tunnelling is possible."""
1210
+ if np.isclose(x1, x0, rtol=1e-5, atol=1e-8).all():
1211
+ raise errors.PotentialError(
1212
+ "It was tried to calculate tunneling between two identical minima.",
1213
+ "no barrier",
1214
+ )
1215
+ if V_(x0) <= V_(x1):
1216
+ raise errors.PotentialError(
1217
+ "V(phi_metaMin) <= V(phi_absMin); tunneling cannot occur.",
1218
+ "stable, not metastable",
1219
+ )
1220
+
1221
+
1222
+ def _execute_full_tunneling(
1223
+ T,
1224
+ V_,
1225
+ dV_,
1226
+ tdict,
1227
+ settings,
1228
+ verbose,
1229
+ conversionFactor,
1230
+ ):
1231
+ """Try to compute the instanton and translate known failure modes."""
1232
+ try:
1233
+ result = fullTunneling([tdict['low_vev'], tdict['high_vev']], V_, dV_, callback_data=T, verbose=verbose, **settings)
1234
+ return {
1235
+ 'instanton': result,
1236
+ 'action': result.action,
1237
+ 'trantype': 1,
1238
+ }, False
1239
+ except errors.PotentialError as err:
1240
+ if err.error_type == "no barrier":
1241
+ if verbose:
1242
+ print(
1243
+ "Tunneling at T = ",
1244
+ T * conversionFactor,
1245
+ "GeV failed because there is no potential barrier.",
1246
+ )
1247
+ return {'action': 0, 'trantype': 2, 'instanton': None}, False
1248
+ if err.error_type == "stable, not metastable":
1249
+ if verbose:
1250
+ print(
1251
+ "Tunneling at T = ",
1252
+ T * conversionFactor,
1253
+ "GeV failed because the potential is stable and not metastable.",
1254
+ )
1255
+ return {'action': np.inf, 'trantype': 0, 'instanton': None}, False
1256
+ if verbose:
1257
+ print("Unexpected error message: ", err.args[1])
1258
+ raise errors.UnexpectedError(
1259
+ "In _tunnelFromPhaseAtT: Unexpected error message: %s" % err.args[1]
1260
+ ) from err
1261
+ except errors.ZeroActionError:
1262
+ if verbose:
1263
+ print(
1264
+ "Tunneling at T =",
1265
+ T * conversionFactor,
1266
+ "GeV failed because the potential barrer is too small. Returning zero action, which corresponds to a positive infinity nucleation criterion.",
1267
+ )
1268
+ return {'action': 0, 'trantype': 2, 'instanton': None}, False
1269
+ except errors.InfiniteActionError:
1270
+ if verbose:
1271
+ print(
1272
+ "Tunneling at T =",
1273
+ T * conversionFactor,
1274
+ "GeV failed because the potential barrier is too high. Returning infinite action, which corresponds to a negative infinity nucleation criterion.",
1275
+ )
1276
+ return {'action': np.inf, 'trantype': 0, 'instanton': None}, False
1277
+ except errors.SplineError as err:
1278
+ if verbose:
1279
+ print(
1280
+ "Tunneling at T = ",
1281
+ T * conversionFactor,
1282
+ "GeV failed because the potential spline could not be constructed ("
1283
+ + err.args[0]
1284
+ + "). Treating this as no tunnelling.",
1285
+ )
1286
+ return {'action': np.inf, 'trantype': 0, 'instanton': None}, True
1287
+ except Exception as err:
1288
+ if verbose:
1289
+ print(
1290
+ "Tunneling at T =",
1291
+ T * conversionFactor,
1292
+ "GeV failed with error: ",
1293
+ err,
1294
+ )
1295
+ raise
1296
+
1297
+
1298
+ def bounceAction(
1299
+ T: float,
1300
+ V: Callable[[np.ndarray, float], np.ndarray],
1301
+ dV: Callable[[np.ndarray, float], np.ndarray],
1302
+ outdict: dict,
1303
+ tdict: dict,
1304
+ verbose: bool = False,
1305
+ conversionFactor: float = 1.0,
1306
+ **fullTunneling_params,
1307
+ ) -> dict:
1308
+ """
1309
+ Compute the 1D bounce action and update the tunnelling dictionaries.
1310
+
1311
+ Parameters
1312
+ ----------
1313
+ T : float
1314
+ Temperature at which to compute the bounce action.
1315
+ V : callable
1316
+ Potential function. Input arrays have shape ``(num_points, N_dim)`` and
1317
+ the output should have shape ``(num_points,)``.
1318
+ dV : callable
1319
+ Gradient of the potential with the same input shape as `V`. The output
1320
+ should have shape ``(num_points, N_dim)``.
1321
+ outdict : dict
1322
+ A dictionary mapping temperatures to tunnelling results. If the
1323
+ result for temperature `T` is already in this dictionary, it is
1324
+ returned immediately.
1325
+ tdict : dict
1326
+ Dictionary describing the tunnelling phase at temperature `T`. Must
1327
+ contain the keys ``'low_vev'`` and ``'high_vev'`` for the field values of
1328
+ the two minima.
1329
+ verbose : bool, optional
1330
+ If True, print diagnostic messages while computing the bounce action.
1331
+ conversionFactor : float, optional
1332
+ Factor to convert the temperature from internal units to GeV when
1333
+ printing messages.
1334
+ fullTunneling_params : dict, optional
1335
+ Extra parameters to pass to :func:`fullTunneling`. The function accepts
1336
+ them via ``**fullTunneling_params`` and forwards the dictionary.
1337
+
1338
+ Returns
1339
+ -------
1340
+ outdict : dict
1341
+ The updated output dictionary, with the result for temperature `T`
1342
+ stored under that key.
1343
+ """
1344
+ if T in outdict:
1345
+ return outdict
1346
+
1347
+ def V_(x, T=T, V=V):
1348
+ return V(x, T)
1349
+
1350
+ def dV_(x, T=T, dV=dV):
1351
+ return dV(x, T)
1352
+
1353
+ x1, x0 = tdict['low_vev'], tdict['high_vev']
1354
+
1355
+ try:
1356
+ _validate_tunneling_path(x0, x1, V_)
1357
+ except errors.PotentialError as err:
1358
+ if verbose:
1359
+ print(
1360
+ "Tunneling at T = ",
1361
+ T * conversionFactor,
1362
+ "GeV failed during path validation: ",
1363
+ err.args[0],
1364
+ )
1365
+ if err.error_type == "no barrier":
1366
+ tdict['action'] = 0
1367
+ tdict['trantype'] = 2
1368
+ tdict['instanton'] = None
1369
+ elif err.error_type == "stable, not metastable":
1370
+ tdict['action'] = np.inf
1371
+ tdict['trantype'] = 0
1372
+ tdict['instanton'] = None
1373
+ else:
1374
+ raise errors.UnexpectedError(
1375
+ "In bounceAction: Unexpected error message: %s" % err.args[0]
1376
+ ) from err
1377
+ outdict[T] = tdict
1378
+ return outdict
1379
+
1380
+ # Run the full tunnelling routine and translate the outcome into
1381
+ # the structure expected by the downstream percolation code.
1382
+ result, early_exit = _execute_full_tunneling(
1383
+ T,
1384
+ V_,
1385
+ dV_,
1386
+ tdict,
1387
+ fullTunneling_params,
1388
+ verbose,
1389
+ conversionFactor,
1390
+ )
1391
+
1392
+ tdict['instanton'] = result.get('instanton')
1393
+ tdict['action'] = result['action']
1394
+ tdict['trantype'] = result['trantype']
1395
+
1396
+ if tdict['action'] < 0:
1397
+ if verbose:
1398
+ print(
1399
+ "Tunneling at T = ",
1400
+ T * conversionFactor,
1401
+ "GeV failed because the action was computed as negative. This can happen if the potential barrier is extremely shallow. Returning zero action, which corresponds to a second-order transition.",
1402
+ )
1403
+ tdict['action'] = 0
1404
+ tdict['trantype'] = 2
1405
+
1406
+ outdict[T] = tdict
1407
+
1408
+ # When a spline failure occurs we skip downstream calculations, but still
1409
+ # provide the partial dictionary to avoid repeated work.
1410
+ if early_exit:
1411
+ return outdict
1412
+
1413
+ return outdict
1414
+
1415
+
1416
+ def bounceSolution(pot, T: float, xstart: np.ndarray, xend: np.ndarray,
1417
+ verbose: bool = False):
1418
+ """Compute the instanton solution for tunneling from `xstart`
1419
+ to `xend`. Return the action and the instanton solution
1420
+ (bubble profile).
1421
+
1422
+ Parameters
1423
+ ----------
1424
+ pot : generic_potential
1425
+ Effective potential
1426
+ T : float
1427
+ Temperature at which to compute the bounce action.
1428
+ xstart: np.ndarray
1429
+ high temperature minimum
1430
+ xend: np.ndarray
1431
+ low temperature minimum
1432
+ verbose : bool, optional
1433
+ If True, print diagnostic messages while computing the bounce action.
1434
+
1435
+ Returns
1436
+ -------
1437
+ profile1D : namedtuple
1438
+ The return value from ``tunneling_class.findProfile()``.
1439
+ Phi : array_like
1440
+ The points that constitute the final deformed path. They are in
1441
+ one-to-one correspondence with the points in `profile1D`.
1442
+ action : float
1443
+ The Euclidean action of the instanton.
1444
+ fRatio : float
1445
+ A ratio of the largest transverse force on the final path relative to
1446
+ the largest potential gradient. This would be zero if the solution were
1447
+ perfect.
1448
+ saved_steps : list
1449
+ A list of lists, with each sub-list containing the saved steps for each
1450
+ deformation. Only written to if `save_all_steps` is True.
1451
+ """
1452
+
1453
+ def V_(x, T=T):
1454
+ return pot.Vtot(x, T, include_radiation=False, include_decoupled=False)
1455
+
1456
+ def dV_(x, T=T):
1457
+ return pot.gradV(x, T)
1458
+
1459
+ result = fullTunneling([xend, xstart], V_, dV_, callback_data=T,
1460
+ **pot.config.tracingConf.tunneling_params)
1461
+
1462
+ return result