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,298 @@
1
+ """
2
+ Coleman–Weinberg gradient and Hessian utilities.
3
+
4
+ The functions defined here implement the analytic derivative formulas used in
5
+ BSMPT and ported into TransitionListener. They operate on the curvature
6
+ tensors evaluated at the vacuum configuration and provide gradient / Hessian
7
+ matrices expressed in the physical field basis (i.e. after rotating into the
8
+ mass eigenstates).
9
+
10
+ Part of TransitionListener v2.0
11
+ Documentation: https://tasillo.de/TransitionListener/
12
+
13
+ Authors:
14
+ Jonas Matuszak <jonas.matuszak@kit.edu>
15
+ Carlo Tasillo <carlo.tasillo@ific.uv.es>
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ from typing import Tuple
22
+
23
+ import numpy as np
24
+
25
+ from transitionlistener.constants import CW_CONSTANTS
26
+ from .tensors import ensure_symmetric
27
+
28
+ EPS_LOOP = 1.0 / (16.0 * math.pi * math.pi)
29
+
30
+
31
+ def _log_term(m2: float, scale_sq: float, counterterm: float) -> float:
32
+ r"""Return the Coleman-Weinberg logarithmic kernel
33
+ :math:`\log(m^2/\mu^2) - c + 1/2`, regularised at ``m^2 = 0``."""
34
+ if m2 == 0.0:
35
+ return -counterterm + 0.5
36
+ return math.log(m2 / scale_sq) - counterterm + 0.5
37
+
38
+
39
+ def _fbase(m2a: float, m2b: float, scale: float) -> float:
40
+ """
41
+ Helper matching ``Class_Potential_Origin::fbase`` in BSMPT.
42
+ """
43
+
44
+ if m2a == 0.0 and m2b == 0.0:
45
+ return 1.0
46
+
47
+ tol = 1e-5
48
+ log_a = math.log(m2a) - 2.0 * math.log(scale) if m2a != 0.0 else 0.0
49
+ if abs(m2a - m2b) > tol:
50
+ log_b = math.log(m2b) - 2.0 * math.log(scale) if m2b != 0.0 else 0.0
51
+ if m2a == 0.0:
52
+ return log_b
53
+ if m2b == 0.0:
54
+ return log_a
55
+ return (log_a * m2a - log_b * m2b) / (m2a - m2b)
56
+ return 1.0 + log_a
57
+
58
+
59
+ def coleman_weinberg_derivatives(
60
+ h2: np.ndarray,
61
+ h3: np.ndarray,
62
+ h4: np.ndarray,
63
+ gauge_curv: np.ndarray,
64
+ quark_curv: np.ndarray,
65
+ lepton_curv: np.ndarray,
66
+ vev: np.ndarray,
67
+ *,
68
+ scale: float,
69
+ scalar_threshold: float = 1e-5,
70
+ fermion_threshold: float = 1e-10,
71
+ ) -> Tuple[np.ndarray, np.ndarray]:
72
+ """
73
+ Compute the Coleman–Weinberg gradient and Hessian in the physical basis.
74
+
75
+ Parameters
76
+ ----------
77
+ h2, h3, h4:
78
+ Higgs curvature tensors (quadratic, cubic, quartic) in the gauge basis.
79
+ gauge_curv:
80
+ Gauge curvature tensor :math:`G_{abij}`.
81
+ quark_curv, lepton_curv:
82
+ Fermionic Yukawa curvature tensors :math:`Y_{ij\\alpha}`.
83
+ vev:
84
+ Vacuum expectation value vector in the Higgs field basis (GeV).
85
+ scale:
86
+ Renormalisation scale :math:`\\mu` used in the Coleman–Weinberg logs.
87
+ scalar_threshold, fermion_threshold:
88
+ Numerical cut-offs below which eigenvalues are treated as zero.
89
+
90
+ Returns
91
+ -------
92
+ (gradient, hessian):
93
+ Coleman–Weinberg first and second derivatives in the physical basis,
94
+ expressed in the same units as the input tensors. The overall
95
+ Coleman–Weinberg loop factor :math:`1/(16 \\pi^2)` is included.
96
+ """
97
+
98
+ scale_sq = scale * scale
99
+
100
+ # ------------------------------------------------------------------
101
+ # Mass matrices and rotations
102
+ # ------------------------------------------------------------------
103
+ mass_h = h2.copy()
104
+ for k in range(vev.shape[0]):
105
+ mass_h += h3[:, :, k] * vev[k]
106
+ for k in range(vev.shape[0]):
107
+ for l in range(vev.shape[0]):
108
+ mass_h += 0.5 * h4[:, :, k, l] * vev[k] * vev[l]
109
+
110
+ mass_g = np.zeros((gauge_curv.shape[0], gauge_curv.shape[1]), dtype=float)
111
+ for a in range(gauge_curv.shape[0]):
112
+ for b in range(gauge_curv.shape[1]):
113
+ total = 0.0
114
+ for i in range(vev.shape[0]):
115
+ for j in range(vev.shape[0]):
116
+ total += gauge_curv[a, b, i, j] * vev[i] * vev[j]
117
+ mass_g[a, b] = 0.5 * total
118
+
119
+ MIJ_quark = np.tensordot(quark_curv, vev, axes=(2, 0))
120
+ MIJ_lepton = np.tensordot(lepton_curv, vev, axes=(2, 0))
121
+
122
+ mass_quark = MIJ_quark.conj().T @ MIJ_quark
123
+ mass_lepton = MIJ_lepton.conj().T @ MIJ_lepton
124
+
125
+ eigvals_h, eigvecs_h = np.linalg.eigh(mass_h)
126
+ HiggsRot = np.real_if_close(eigvecs_h.T, tol=1e8)
127
+ mass_h_sq = eigvals_h.copy()
128
+ mass_h_sq[np.abs(mass_h_sq) < scalar_threshold] = 0.0
129
+
130
+ eigvals_g, eigvecs_g = np.linalg.eigh(mass_g)
131
+ GaugeRot = np.real_if_close(eigvecs_g.T, tol=1e8)
132
+ mass_g_sq = eigvals_g.copy()
133
+ mass_g_sq[np.abs(mass_g_sq) < scalar_threshold] = 0.0
134
+
135
+ eigvals_q, eigvecs_q = np.linalg.eigh(mass_quark)
136
+ QuarkRot = np.real_if_close(eigvecs_q.T, tol=1e8)
137
+ mass_quark_sq = eigvals_q.real
138
+ mass_quark_sq[np.abs(mass_quark_sq) < fermion_threshold] = 0.0
139
+
140
+ eigvals_l, eigvecs_l = np.linalg.eigh(mass_lepton)
141
+ LepRot = np.real_if_close(eigvecs_l.T, tol=1e8)
142
+ mass_lepton_sq = eigvals_l.real
143
+ mass_lepton_sq[np.abs(mass_lepton_sq) < fermion_threshold] = 0.0
144
+
145
+ # ------------------------------------------------------------------
146
+ # Physical couplings
147
+ # ------------------------------------------------------------------
148
+ LambdaGauge3 = np.einsum("abij,j->abi", gauge_curv, vev, optimize=True)
149
+ LambdaHiggs3 = h3 + np.einsum("abij,j->abi", h4, vev, optimize=True)
150
+
151
+ LambdaQuark3 = (
152
+ np.einsum("ilk,lj->ijk", np.conjugate(quark_curv), MIJ_quark, optimize=True)
153
+ + np.einsum("il,ljk->ijk", np.conjugate(MIJ_quark), quark_curv, optimize=True)
154
+ )
155
+ LambdaQuark4 = (
156
+ np.einsum("ilk,ljm->ijkm", np.conjugate(quark_curv), quark_curv, optimize=True)
157
+ + np.einsum("ilm,ljk->ijkm", np.conjugate(quark_curv), quark_curv, optimize=True)
158
+ )
159
+
160
+ LambdaLepton3 = (
161
+ np.einsum("ilk,lj->ijk", np.conjugate(lepton_curv), MIJ_lepton, optimize=True)
162
+ + np.einsum("il,ljk->ijk", np.conjugate(MIJ_lepton), lepton_curv, optimize=True)
163
+ )
164
+ LambdaLepton4 = (
165
+ np.einsum("ilk,ljm->ijkm", np.conjugate(lepton_curv), lepton_curv, optimize=True)
166
+ + np.einsum("ilm,ljk->ijkm", np.conjugate(lepton_curv), lepton_curv, optimize=True)
167
+ )
168
+
169
+ Couplings_Higgs_Triple = np.einsum(
170
+ "pi,qj,rk,ijk->pqr", HiggsRot, HiggsRot, HiggsRot, LambdaHiggs3, optimize=True
171
+ )
172
+ Couplings_Higgs_Quartic = np.einsum(
173
+ "pi,qj,rk,sl,ijkl->pqrs", HiggsRot, HiggsRot, HiggsRot, HiggsRot, h4, optimize=True
174
+ )
175
+ Couplings_Gauge_Higgs_21 = np.einsum(
176
+ "pa,qb,ri,abi->pqr", GaugeRot, GaugeRot, HiggsRot, LambdaGauge3, optimize=True
177
+ )
178
+ Couplings_Gauge_Higgs_22 = np.einsum(
179
+ "pa,qb,ri,sj,abij->pqrs", GaugeRot, GaugeRot, HiggsRot, HiggsRot, gauge_curv, optimize=True
180
+ )
181
+ Couplings_Quark_Higgs_21 = np.einsum(
182
+ "pa,qb,ri,abi->pqr", QuarkRot, QuarkRot, HiggsRot, LambdaQuark3, optimize=True
183
+ )
184
+ Couplings_Quark_Higgs_22 = np.einsum(
185
+ "pa,qb,ri,sj,abij->pqrs", QuarkRot, QuarkRot, HiggsRot, HiggsRot, LambdaQuark4, optimize=True
186
+ )
187
+ Couplings_Lepton_Higgs_21 = np.einsum(
188
+ "pa,qb,ri,abi->pqr", LepRot, LepRot, HiggsRot, LambdaLepton3, optimize=True
189
+ )
190
+ Couplings_Lepton_Higgs_22 = np.einsum(
191
+ "pa,qb,ri,sj,abij->pqrs", LepRot, LepRot, HiggsRot, HiggsRot, LambdaLepton4, optimize=True
192
+ )
193
+
194
+ # ------------------------------------------------------------------
195
+ # Gradient
196
+ # ------------------------------------------------------------------
197
+ grad_mass_basis = np.zeros(vev.shape[0], dtype=float)
198
+ cw_gauge = CW_CONSTANTS["gauge"]
199
+ cw_scalar = CW_CONSTANTS["scalar"]
200
+ cw_fermion = CW_CONSTANTS["fermion"]
201
+
202
+ for i in range(vev.shape[0]):
203
+ tmp = 0.0
204
+ for a in range(mass_g_sq.shape[0]):
205
+ m2 = mass_g_sq[a]
206
+ if m2 != 0.0:
207
+ coup = Couplings_Gauge_Higgs_21[a, a, i]
208
+ tmp += 1.5 * float(coup) * m2 * _log_term(m2, scale_sq, cw_gauge)
209
+ for a in range(mass_h_sq.shape[0]):
210
+ m2 = mass_h_sq[a]
211
+ if m2 != 0.0:
212
+ coup = Couplings_Higgs_Triple[a, a, i]
213
+ tmp += 0.5 * float(coup) * m2 * _log_term(m2, scale_sq, cw_scalar)
214
+ for a in range(mass_quark_sq.shape[0]):
215
+ m2 = mass_quark_sq[a]
216
+ if m2 != 0.0:
217
+ coup = Couplings_Quark_Higgs_21[a, a, i].real
218
+ tmp -= 3.0 * coup * m2 * _log_term(m2, scale_sq, cw_fermion)
219
+ for a in range(mass_lepton_sq.shape[0]):
220
+ m2 = mass_lepton_sq[a]
221
+ if m2 != 0.0:
222
+ coup = Couplings_Lepton_Higgs_21[a, a, i].real
223
+ tmp -= 1.0 * coup * m2 * _log_term(m2, scale_sq, cw_fermion)
224
+ grad_mass_basis[i] = tmp
225
+
226
+ grad_phys = HiggsRot.T @ grad_mass_basis
227
+ grad_phys = np.real_if_close(grad_phys * EPS_LOOP, tol=1e8)
228
+
229
+ # ------------------------------------------------------------------
230
+ # Hessian
231
+ # ------------------------------------------------------------------
232
+ storage = np.zeros((vev.shape[0], vev.shape[0]), dtype=float)
233
+ for i in range(vev.shape[0]):
234
+ for j in range(vev.shape[0]):
235
+ total = 0.0
236
+ # Gauge sector
237
+ block = 0.0
238
+ for a in range(mass_g_sq.shape[0]):
239
+ for b in range(mass_g_sq.shape[0]):
240
+ coup1 = Couplings_Gauge_Higgs_21[a, b, i]
241
+ coup2 = Couplings_Gauge_Higgs_21[b, a, j]
242
+ br = _fbase(mass_g_sq[a], mass_g_sq[b], scale) - cw_gauge + 0.5
243
+ block += float(coup1) * float(coup2) * br
244
+ if mass_g_sq[a] != 0.0:
245
+ coup = Couplings_Gauge_Higgs_22[a, a, i, j]
246
+ block += float(coup) * mass_g_sq[a] * _log_term(mass_g_sq[a], scale_sq, cw_gauge)
247
+ total += 1.5 * block
248
+
249
+ # Scalar sector
250
+ block = 0.0
251
+ for a in range(mass_h_sq.shape[0]):
252
+ for b in range(mass_h_sq.shape[0]):
253
+ coup1 = Couplings_Higgs_Triple[a, b, i]
254
+ coup2 = Couplings_Higgs_Triple[b, a, j]
255
+ br = _fbase(mass_h_sq[a], mass_h_sq[b], scale) - cw_scalar + 0.5
256
+ block += float(coup1) * float(coup2) * br
257
+ if mass_h_sq[a] != 0.0:
258
+ coup = Couplings_Higgs_Quartic[a, a, i, j]
259
+ block += float(coup) * mass_h_sq[a] * _log_term(mass_h_sq[a], scale_sq, cw_scalar)
260
+ total += 0.5 * block
261
+
262
+ # Quark sector
263
+ block = 0.0
264
+ for a in range(mass_quark_sq.shape[0]):
265
+ for b in range(mass_quark_sq.shape[0]):
266
+ coup = (
267
+ Couplings_Quark_Higgs_21[a, b, i]
268
+ * Couplings_Quark_Higgs_21[b, a, j]
269
+ ).real
270
+ br = _fbase(mass_quark_sq[a], mass_quark_sq[b], scale) - cw_fermion + 0.5
271
+ block += coup * br
272
+ if mass_quark_sq[a] != 0.0:
273
+ coup = Couplings_Quark_Higgs_22[a, a, i, j].real
274
+ block += coup * mass_quark_sq[a] * _log_term(mass_quark_sq[a], scale_sq, cw_fermion)
275
+ total -= 3.0 * block
276
+
277
+ # Lepton sector
278
+ block = 0.0
279
+ for a in range(mass_lepton_sq.shape[0]):
280
+ for b in range(mass_lepton_sq.shape[0]):
281
+ coup = (
282
+ Couplings_Lepton_Higgs_21[a, b, i]
283
+ * Couplings_Lepton_Higgs_21[b, a, j]
284
+ ).real
285
+ br = _fbase(mass_lepton_sq[a], mass_lepton_sq[b], scale) - cw_fermion + 0.5
286
+ block += coup * br
287
+ if mass_lepton_sq[a] != 0.0:
288
+ coup = Couplings_Lepton_Higgs_22[a, a, i, j].real
289
+ block += coup * mass_lepton_sq[a] * _log_term(mass_lepton_sq[a], scale_sq, cw_fermion)
290
+ total -= block
291
+
292
+ storage[i, j] = total
293
+
294
+ storage = ensure_symmetric(storage)
295
+ hess_phys = HiggsRot.T @ storage @ HiggsRot
296
+ hess_phys = np.real_if_close(hess_phys * EPS_LOOP, tol=1e8)
297
+
298
+ return grad_phys, ensure_symmetric(hess_phys)
@@ -0,0 +1,343 @@
1
+ """
2
+ Low-level helpers for symbolic curvature tensors.
3
+
4
+ The utilities provided here centralise the JSON I/O that we use to ship SymPy
5
+ expressions from the generator stage to the runtime evaluation. They also
6
+ offer a thin numerical wrapper around each expression so that models can keep
7
+ their own code short and declarative.
8
+
9
+ Part of TransitionListener v2.0
10
+ Documentation: https://tasillo.de/TransitionListener/
11
+
12
+ Authors:
13
+ Jonas Matuszak <jonas.matuszak@kit.edu>
14
+ Carlo Tasillo <carlo.tasillo@ific.uv.es>
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from functools import lru_cache
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING, Any, Mapping
22
+
23
+ import json
24
+ from itertools import permutations
25
+ import numpy as np
26
+
27
+ from transitionlistener import errors
28
+
29
+ if TYPE_CHECKING:
30
+ import sympy as sp
31
+
32
+
33
+ def _require_sympy():
34
+ """Import SymPy on demand and raise a targeted error when it is unavailable."""
35
+ try:
36
+ import sympy as sp
37
+ except ModuleNotFoundError as exc:
38
+ raise ModuleNotFoundError(
39
+ "SymPy is required for symbolic counterterm tensor evaluation. "
40
+ "Install `sympy` to use SymPy-based counterterm models."
41
+ ) from exc
42
+ return sp
43
+
44
+
45
+ def _is_index_key(key: str) -> bool:
46
+ """Return whether ``key`` encodes a tensor index tuple such as ``"(0, 1)"``."""
47
+ key = key.strip()
48
+ return key.startswith("(") and key.endswith(")")
49
+
50
+
51
+ def _parse_index_key(key: str) -> tuple[int, ...]:
52
+ """Parse a stringified tensor-index tuple back into integers."""
53
+ stripped = key.strip()[1:-1]
54
+ if not stripped:
55
+ return tuple()
56
+ return tuple(int(part.strip()) for part in stripped.split(","))
57
+
58
+
59
+ def _convert_tensor_dict(obj: Any) -> Any:
60
+ """Recursively turn JSON-loaded tensor data back into SymPy expressions."""
61
+ sp = _require_sympy()
62
+ if isinstance(obj, dict):
63
+ converted = {}
64
+ for key, value in obj.items():
65
+ if _is_index_key(key):
66
+ converted[_parse_index_key(key)] = _convert_tensor_dict(value)
67
+ else:
68
+ converted[key] = _convert_tensor_dict(value)
69
+ return converted
70
+ if isinstance(obj, list):
71
+ return [_convert_tensor_dict(item) for item in obj]
72
+ if isinstance(obj, str):
73
+ return sp.sympify(obj, evaluate=False)
74
+ return obj
75
+
76
+
77
+ def _initialise_parameter_symbols(
78
+ metadata: dict[str, Any],
79
+ *,
80
+ complex_parameters: tuple[str, ...] = (),
81
+ ) -> dict[str, sp.Symbol]:
82
+ """Construct the SymPy symbols corresponding to the stored metadata labels."""
83
+ sp = _require_sympy()
84
+ parameter_aliases = metadata.get("parameter_symbol_names", {})
85
+ parameter_symbols: dict[str, sp.Symbol] = {}
86
+ for name in metadata.get("parameters", []):
87
+ alias = parameter_aliases.get(name, name)
88
+ alias_expr = sp.sympify(alias)
89
+ if isinstance(alias_expr, sp.Symbol):
90
+ base_name = alias_expr.name
91
+ else:
92
+ base_name = str(alias)
93
+ alias_expr = sp.Symbol(base_name)
94
+ is_complex = base_name in complex_parameters
95
+ symbol = sp.Symbol(base_name) if is_complex else sp.Symbol(base_name, real=True)
96
+ for tag in {name, base_name, str(alias)}:
97
+ parameter_symbols[tag] = symbol
98
+ parameter_symbols[alias_expr] = symbol
99
+ metadata["parameter_symbols"] = parameter_symbols
100
+ return parameter_symbols
101
+
102
+
103
+ @lru_cache(maxsize=None)
104
+ def load_tensor_dataset(
105
+ tensor_file: Path,
106
+ *,
107
+ complex_parameters: tuple[str, ...] = (
108
+ "Vud",
109
+ "Vus",
110
+ "Vub",
111
+ "Vcd",
112
+ "Vcs",
113
+ "Vcb",
114
+ "Vtd",
115
+ "Vts",
116
+ "Vtb",
117
+ ),
118
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
119
+ """
120
+ Load a JSON tensor dataset and return SymPy expressions ready for evaluation.
121
+
122
+ Parameters
123
+ ----------
124
+ tensor_file:
125
+ Absolute path to the JSON document produced by the counterterm generator.
126
+ complex_parameters:
127
+ Iterable with the subset of parameters that need complex-valued symbols.
128
+
129
+ Returns
130
+ -------
131
+ (metadata, tensors)
132
+ Metadata dictionary (augmented with ``parameter_symbols``) and a nested
133
+ dictionary of SymPy expressions keyed by index tuples.
134
+ """
135
+
136
+ if not tensor_file.exists():
137
+ raise errors.InitPotentialError(
138
+ f"Generated tensor file not found: {tensor_file}. "
139
+ "Run the counterterm generator to create it."
140
+ )
141
+
142
+ with tensor_file.open("r", encoding="utf-8") as fh:
143
+ raw = json.load(fh)
144
+
145
+ metadata = raw.get("metadata", {})
146
+ _initialise_parameter_symbols(metadata, complex_parameters=complex_parameters)
147
+ tensors = _convert_tensor_dict(raw.get("tensors", {}))
148
+ return metadata, tensors
149
+
150
+
151
+ def build_substitutions(
152
+ metadata: Mapping[str, Any],
153
+ parameter_values: Mapping[str, complex],
154
+ ) -> dict[sp.Symbol, complex]:
155
+ """
156
+ Construct the substitution dictionary used during tensor evaluation.
157
+
158
+ Parameters
159
+ ----------
160
+ metadata:
161
+ Metadata dictionary returned by :func:`load_tensor_dataset`.
162
+ parameter_values:
163
+ Mapping from parameter identifiers (as stored in the metadata) to the
164
+ numerical values provided by the model.
165
+
166
+ Returns
167
+ -------
168
+ dict
169
+ Mapping ``sympy.Symbol -> value`` ready to be passed to
170
+ :func:`evaluate_expression`.
171
+ """
172
+
173
+ sp = _require_sympy()
174
+ subs: dict[sp.Symbol, complex] = {}
175
+ param_symbols: dict[str, sp.Symbol] = dict(metadata.get("parameter_symbols", {}))
176
+ aliases = metadata.get("parameter_symbol_names", {})
177
+
178
+ def assign(label: str, value: complex) -> None:
179
+ symbol = param_symbols.get(label)
180
+ if symbol is None:
181
+ symbol = sp.Symbol(label)
182
+ param_symbols[label] = symbol
183
+ subs[symbol] = value
184
+ # Also populate the alias to avoid round trips.
185
+ alias_label = aliases.get(label)
186
+ if alias_label is not None:
187
+ alias_symbol = param_symbols.get(alias_label)
188
+ if alias_symbol is None:
189
+ alias_symbol = sp.Symbol(alias_label)
190
+ param_symbols[alias_label] = alias_symbol
191
+ subs[alias_symbol] = value
192
+
193
+ for key, value in parameter_values.items():
194
+ assign(str(key), complex(value))
195
+
196
+ metadata["parameter_symbols"] = param_symbols # type: ignore[index]
197
+ return subs
198
+
199
+
200
+ def evaluate_expression(
201
+ expr: sp.Expr,
202
+ substitutions: Mapping[sp.Symbol, complex],
203
+ *,
204
+ extra_substitutions: Mapping[str | sp.Symbol, complex] | None = None,
205
+ ) -> complex | float:
206
+ """
207
+ Evaluate a tensor entry to floating-point precision.
208
+
209
+ Parameters
210
+ ----------
211
+ expr:
212
+ SymPy expression representing the tensor component.
213
+ substitutions:
214
+ Base substitution map generated by :func:`build_substitutions`.
215
+ extra_substitutions:
216
+ Optional field-dependent substitutions that should override the base map.
217
+
218
+ Returns
219
+ -------
220
+ complex
221
+ Numerical value of the expression. Purely real results are returned as
222
+ ``float`` for convenience.
223
+ """
224
+
225
+ sp = _require_sympy()
226
+ if expr is None:
227
+ return 0.0
228
+
229
+ subs = dict(substitutions)
230
+ if extra_substitutions:
231
+ for key, value in extra_substitutions.items():
232
+ symbol = key if isinstance(key, sp.Symbol) else sp.Symbol(str(key))
233
+ subs[symbol] = complex(value)
234
+
235
+ evaluated = sp.N(expr.subs(subs), 30)
236
+ free_symbols = getattr(evaluated, "free_symbols", set())
237
+ if free_symbols:
238
+ missing = ", ".join(str(sym) for sym in sorted(free_symbols, key=str))
239
+ raise ValueError(f"Unresolved symbols in tensor evaluation: {missing} (expression: {expr})")
240
+
241
+ try:
242
+ value = complex(evaluated)
243
+ except TypeError:
244
+ real_part = float(sp.N(sp.re(evaluated), 30))
245
+ imag_part = float(sp.N(sp.im(evaluated), 30))
246
+ value = complex(real_part, imag_part)
247
+
248
+ if abs(value.imag) < 1e-12:
249
+ return float(value.real)
250
+ if abs(value.real) < 1e-12:
251
+ return complex(0.0, value.imag)
252
+ return value
253
+
254
+
255
+ def ensure_symmetric(matrix):
256
+ """
257
+ Symmetrise a square numerical matrix.
258
+
259
+ This helper is used to enforce the hermiticity of Coleman–Weinberg Hessians
260
+ after evaluating them numerically.
261
+ """
262
+
263
+ return 0.5 * (matrix + matrix.T)
264
+
265
+
266
+ def _determine_dim(keys, positions):
267
+ """Infer an array dimension from the largest index stored in ``keys``."""
268
+ max_index = 0
269
+ for key in keys:
270
+ for pos in positions:
271
+ max_index = max(max_index, key[pos])
272
+ return max_index + 1
273
+
274
+
275
+ def evaluate_curvature_arrays(
276
+ tensors: Mapping[str, Any],
277
+ metadata: Mapping[str, Any],
278
+ parameter_values: Mapping[str, complex],
279
+ *,
280
+ yukawa_key: str,
281
+ ) -> dict[str, np.ndarray]:
282
+ """
283
+ Evaluate the curvature tensors stored in ``tensors`` using ``parameter_values``.
284
+ """
285
+
286
+ subs = build_substitutions(metadata, parameter_values)
287
+ n_h = len(metadata.get("field_basis", []))
288
+ if n_h == 0:
289
+ n_h = _determine_dim(tensors.get("Curvature_Higgs_L2", {}).keys(), (0, 1))
290
+
291
+ h2 = np.zeros((n_h, n_h), dtype=float)
292
+ for (i, j), expr in tensors.get("Curvature_Higgs_L2", {}).items():
293
+ value = float(evaluate_expression(expr, subs))
294
+ h2[i, j] = value
295
+ h2[j, i] = value
296
+
297
+ h3 = np.zeros((n_h, n_h, n_h), dtype=float)
298
+ for key, expr in tensors.get("Curvature_Higgs_L3", {}).items():
299
+ value = float(evaluate_expression(expr, subs))
300
+ for perm in set(permutations(key)):
301
+ h3[perm] = value
302
+
303
+ h4 = np.zeros((n_h, n_h, n_h, n_h), dtype=float)
304
+ for key, expr in tensors.get("Curvature_Higgs_L4", {}).items():
305
+ value = float(evaluate_expression(expr, subs))
306
+ for perm in set(permutations(key)):
307
+ h4[perm] = value
308
+
309
+ gauge_data = tensors.get("Curvature_Gauge_G2H2", {})
310
+ if gauge_data:
311
+ n_g = _determine_dim(gauge_data.keys(), (0, 1))
312
+ else:
313
+ n_g = 0
314
+ gauge = np.zeros((n_g, n_g, n_h, n_h), dtype=float)
315
+ for (a, b, i, j), expr in gauge_data.items():
316
+ value = float(evaluate_expression(expr, subs))
317
+ gauge[a, b, i, j] = value
318
+ gauge[a, b, j, i] = value
319
+ gauge[b, a, j, i] = value
320
+ gauge[b, a, i, j] = value
321
+
322
+ quark_data = tensors.get("Curvature_Quark_F2H1", {}).get(yukawa_key, {})
323
+ lepton_data = tensors.get("Curvature_Lepton_F2H1", {}).get(yukawa_key, {})
324
+
325
+ n_q = _determine_dim(quark_data.keys(), (0, 1)) if quark_data else 0
326
+ n_l = _determine_dim(lepton_data.keys(), (0, 1)) if lepton_data else 0
327
+
328
+ quark = np.zeros((n_q, n_q, n_h), dtype=complex)
329
+ for (i, j, k), expr in quark_data.items():
330
+ quark[i, j, k] = evaluate_expression(expr, subs)
331
+
332
+ lepton = np.zeros((n_l, n_l, n_h), dtype=complex)
333
+ for (i, j, k), expr in lepton_data.items():
334
+ lepton[i, j, k] = evaluate_expression(expr, subs)
335
+
336
+ return {
337
+ "H2": h2,
338
+ "H3": h3,
339
+ "H4": h4,
340
+ "Gauge": gauge,
341
+ "Quark": quark,
342
+ "Lepton": lepton,
343
+ }