diff --git a/benchmarks/CT14_bench.py b/benchmarks/CT14_bench.py
index ce6ca2cb1..31a1e87d3 100644
--- a/benchmarks/CT14_bench.py
+++ b/benchmarks/CT14_bench.py
@@ -5,9 +5,10 @@
whereas the lo family uses LO splitting functions with NLO alphas evolution
"""
+from math import nan
+
from banana import register
-from eko import compatibility
from ekomark.benchmark.runner import Runner
register(__file__)
@@ -15,6 +16,7 @@
base_theory = {
"Qref": 91.1876,
+ "QrefQED": nan,
"mc": 1.3,
"mb": 4.75,
"mt": 172,
diff --git a/benchmarks/CT18_bench.py b/benchmarks/CT18_bench.py
index 0e102c043..cc7c06e47 100644
--- a/benchmarks/CT18_bench.py
+++ b/benchmarks/CT18_bench.py
@@ -12,6 +12,7 @@
base_theory = {
"Qref": 91.1870,
+ "QrefQED": 91.1870,
"mc": 1.3,
"mb": 4.75,
"mt": 172.0,
@@ -53,6 +54,28 @@ def benchmark_nnlo(self, Q0=1.295, Q2grid=(1e4,)):
]
self.run([theory_card], [operator_card], ["CT18NNLO"])
+ def benchmark_nnlo_qed(self, Q0=1.295, Q2grid=(1e4,)):
+ theory_card = base_theory.copy()
+ theory_card.update(
+ {
+ "alphas": 0.118000,
+ "alphaqed": 0.007496,
+ "PTO": 2,
+ "QED": 1,
+ "Q0": Q0,
+ "MaxNfPdf": 5,
+ "MaxNfAs": 5,
+ }
+ )
+ operator_card = {"Q2grid": list(Q2grid)}
+ self.skip_pdfs = lambda _theory: [
+ -6,
+ 6,
+ "Tu8",
+ "Vu8",
+ ]
+ self.run([theory_card], [operator_card], ["CT18qed"])
+
def benchmark_znnlo(self, Q0=1.3, Q2grid=(1e4,)):
theory_card = base_theory.copy()
theory_card.update(
diff --git a/benchmarks/HERA20_bench.py b/benchmarks/HERA20_bench.py
index f7d77fce8..057a25954 100644
--- a/benchmarks/HERA20_bench.py
+++ b/benchmarks/HERA20_bench.py
@@ -2,6 +2,8 @@
Benchmark HERAPDF2.0 pdf family
"""
+from math import nan
+
from banana import register
from eko import interpolation
@@ -12,6 +14,7 @@
base_theory = {
"Qref": 91.1876,
+ "QrefQED": nan,
"mc": 1.43,
"mb": 4.5,
"mt": 173.0,
diff --git a/benchmarks/NNPDF_bench.py b/benchmarks/NNPDF_bench.py
index e0a90ec99..008b0e4a2 100644
--- a/benchmarks/NNPDF_bench.py
+++ b/benchmarks/NNPDF_bench.py
@@ -63,6 +63,33 @@ def benchmark_nlo(self, Q0=1.65, Q2grid=(100,)):
self.run([theory_card], [operator_card], ["NNPDF31_nlo_as_0118"])
+class BenchmarkNNPDF31_luxqed(BenchmarkNNPDF):
+ """Benchmark NNPDF3.1_luxqed"""
+
+ def benchmark_nnlo(self, Q0=1.65, Q2grid=(100,)):
+ theory_card = {
+ **base_theory,
+ "PTO": 2,
+ "QED": 2,
+ "Q0": Q0,
+ }
+ theory_card.update({"ModEv": "iterate-exact", "FNS": "VFNS", "QrefQED": 91.2})
+
+ self.skip_pdfs = lambda _theory: [
+ -6,
+ 6,
+ "Tu8",
+ "Vu8",
+ ]
+
+ operator_card = {
+ **base_operator,
+ "Q2grid": list(Q2grid),
+ "ev_op_iterations": 10,
+ }
+ self.run([theory_card], [operator_card], ["NNPDF31_nnlo_as_0118_luxqed"])
+
+
class BenchmarkNNPDF40(BenchmarkNNPDF):
"""Benchmark NNPDF4.0"""
@@ -128,8 +155,10 @@ def benchmark(self, Q0=1.65, Q2grid=(100,)):
# nn31.benchmark_nlo(Q0=np.sqrt(low2), Q2grid=[10])
# # test backward
# #nn31.benchmark_nlo(Q0=np.sqrt(high2), Q2grid=[low2])
- # nn40 = BenchmarkNNPDF40()
- # # nn40.benchmark_nnlo(Q2grid=[100])
+ nn40 = BenchmarkNNPDF40()
+ nn40.benchmark_nnlo(Q2grid=[100])
# nn40.benchmark_nnlo(Q0=np.sqrt(high2), Q2grid=[low2])
- nnpol = BenchmarkNNPDFpol11()
- nnpol.benchmark(Q0=np.sqrt(low2), Q2grid=[high2])
+ # nnpol = BenchmarkNNPDFpol11()
+ # nnpol.benchmark(Q0=np.sqrt(low2), Q2grid=[high2])
+ # obj = BenchmarkNNPDF31_luxqed()
+ # obj.benchmark_nnlo(Q0=5.0)
diff --git a/benchmarks/apfel_bench.py b/benchmarks/apfel_bench.py
index ee1a39bd4..96251a283 100644
--- a/benchmarks/apfel_bench.py
+++ b/benchmarks/apfel_bench.py
@@ -197,13 +197,137 @@ def benchmark_sv(self, pto, svmode):
self.run(ts, operators.build(operators.apfel_config), ["ToyLH"])
-if __name__ == "__main__":
+class BenchmarkFFNS_qed(ApfelBenchmark):
+ """Benckmark FFNS"""
+
+ ffns_theory = {
+ "Qref": 91.1870,
+ "QrefQED": 91.1870,
+ "mc": 1.3,
+ "mb": 4.75,
+ "mt": 172.0,
+ "FNS": "FFNS",
+ "ModEv": [
+ "EXA",
+ # "EXP",
+ # "TRN",
+ ],
+ "NfFF": 5,
+ "kcThr": 0.0,
+ "kbThr": 0.0,
+ "ktThr": np.inf,
+ "Q0": 5.0,
+ "alphas": 0.118000,
+ "alphaqed": 0.007496,
+ }
+ ffns_theory = tolist(ffns_theory)
+
+ def benchmark_plain(self, pto, qed):
+ """Plain configuration"""
+
+ th = self.ffns_theory.copy()
+ th.update({"PTO": [pto], "QED": [qed]})
+ self.skip_pdfs = lambda _theory: [
+ -6,
+ 6,
+ "Tu8",
+ "Vu8",
+ ]
+ self.run(
+ cartesian_product(th),
+ operators.build(operators.apfel_config),
+ ["NNPDF31_nnlo_as_0118_luxqed"],
+ )
+
+ def benchmark_sv(self, pto, qed, svmode):
+ """Scale Variation"""
+ ts = []
+ th = self.ffns_theory.copy()
+ th.update(
+ {
+ "PTO": [pto],
+ "QED": [qed],
+ "XIR": [np.sqrt(0.5)],
+ "fact_to_ren_scale_ratio": [np.sqrt(2.0)],
+ "ModSV": [svmode],
+ "EScaleVar": [0],
+ }
+ )
+ ts.extend(cartesian_product(th))
+ th = self.ffns_theory.copy()
+ th.update(
+ {
+ "PTO": [pto],
+ "QED": [qed],
+ "XIR": [np.sqrt(2.0)],
+ "fact_to_ren_scale_ratio": [np.sqrt(0.5)],
+ "ModSV": [svmode],
+ "EScaleVar": [0],
+ }
+ )
+ ts.extend(cartesian_product(th))
+ self.skip_pdfs = lambda _theory: [
+ -6,
+ 6,
+ "Tu8",
+ "Vu8",
+ ]
+ self.run(ts, operators.build(operators.apfel_config), ["ToyLH"])
+
+
+class BenchmarkVFNS_qed(ApfelBenchmark):
+ """Benckmark FFNS"""
+
+ vfns_theory = {
+ "Qref": 91.1870,
+ "QrefQED": 91.1870,
+ "mc": 1.3,
+ "mb": 4.75,
+ "mt": 172.0,
+ "FNS": "VFNS",
+ "ModEv": [
+ "EXA",
+ # "EXP",
+ # "TRN",
+ ],
+ "kcThr": 1.0,
+ "kbThr": 1.0,
+ "ktThr": 1.0,
+ "Q0": 1.25,
+ "alphas": 0.118000,
+ "alphaqed": 0.007496,
+ }
+ vfns_theory = tolist(vfns_theory)
+
+ def benchmark_plain(self, pto, qed):
+ """Plain configuration"""
+
+ th = self.vfns_theory.copy()
+ th.update({"PTO": [pto], "QED": [qed]})
+ self.skip_pdfs = lambda _theory: [
+ -6,
+ 6,
+ "Tu8",
+ "Vu8",
+ ]
+ self.run(
+ cartesian_product(th),
+ operators.build(operators.apfel_config),
+ ["NNPDF31_nnlo_as_0118_luxqed"],
+ )
+
+
+if __name__ == "__main__":
# obj = BenchmarkVFNS()
- obj = BenchmarkFFNS()
+ # obj = BenchmarkFFNS()
- obj.benchmark_plain_pol(2)
+ # obj.benchmark_plain_pol(2)
# obj.benchmark_plain(2)
# obj.benchmark_sv(2, "exponentiated")
# obj.benchmark_kthr(2)
# obj.benchmark_msbar(2)
+
+ # obj = BenchmarkFFNS_qed()
+ obj = BenchmarkFFNS_qed()
+ obj.benchmark_plain(2, 2)
diff --git a/benchmarks/lha_paper_bench.py b/benchmarks/lha_paper_bench.py
index e78765a60..316acffdd 100644
--- a/benchmarks/lha_paper_bench.py
+++ b/benchmarks/lha_paper_bench.py
@@ -1,6 +1,8 @@
"""
Benchmark to :cite:`Giele:2002hx` (LO + NLO) and :cite:`Dittmar:2005ed` (NNLO).
"""
+from math import nan
+
import numpy as np
from banana import register
from banana.data import cartesian_product
@@ -27,6 +29,7 @@
"alphas": 0.35, # Eq. (4.55) :cite:`Dittmar:2005ed`
"alphaqed": 0.007496,
"QED": 0,
+ "QrefQED": nan,
}
"""Global theory settings"""
diff --git a/benchmarks/pegasus_bench.py b/benchmarks/pegasus_bench.py
index e3152148a..f417c812d 100644
--- a/benchmarks/pegasus_bench.py
+++ b/benchmarks/pegasus_bench.py
@@ -1,6 +1,8 @@
"""
Benchmark to Pegasus :cite:`Vogt:2004ns`
"""
+from math import nan
+
import numpy as np
from banana import register
from banana.data import cartesian_product
@@ -54,6 +56,7 @@ class BenchmarkVFNS(PegasusBenchmark):
"kbThr": 1.0,
"ktThr": 1.0,
"Qref": np.sqrt(2.0),
+ "QrefQED": nan,
"alphas": 0.35,
"alphaqed": 0.007496,
"QED": 0,
@@ -107,7 +110,9 @@ class BenchmarkFFNS(PegasusBenchmark):
"kbThr": np.inf,
"ktThr": np.inf,
"Qref": np.sqrt(2.0),
+ "QrefQED": nan,
"alphas": 0.35,
+ "alphaqed": 0.007496,
"Q0": np.sqrt(2.0),
}
ffns_theory = tolist(ffns_theory)
@@ -151,7 +156,6 @@ def benchmark_sv(self, pto, svmode):
if __name__ == "__main__":
-
# obj = BenchmarkVFNS()
obj = BenchmarkFFNS()
obj.benchmark_plain_pol(1)
diff --git a/benchmarks/sandbox.py b/benchmarks/sandbox.py
index 721bc30e9..3d332b52f 100644
--- a/benchmarks/sandbox.py
+++ b/benchmarks/sandbox.py
@@ -1,4 +1,6 @@
# pylint: skip-file
+from math import nan
+
import numpy as np
from banana import register
@@ -25,6 +27,7 @@
}
nnpdf_base_theory = {
"Qref": 91.2,
+ "QrefQED": nan,
"mc": 1.51,
"mb": 4.92,
"mt": 172.5,
diff --git a/doc/source/code/IO.rst b/doc/source/code/IO.rst
index 43fe6daa0..b4f1bbaf1 100644
--- a/doc/source/code/IO.rst
+++ b/doc/source/code/IO.rst
@@ -69,7 +69,7 @@ and environment. The benchmark settings are available at :mod:`banana.data.theor
* - ``alphaqed``
- :py:obj:`float`
- Reference value of the electromagnetic coupling :math:`\alpha_{em}`.
- * - ``Qedref``
+ * - ``QrefQED``
- :py:obj:`float`
- Reference scale at which the ``alphaqed`` value is given (in GeV).
* - ``HQ``
diff --git a/doc/source/theory/DGLAP.rst b/doc/source/theory/DGLAP.rst
index b06b7031b..91f7f9e11 100644
--- a/doc/source/theory/DGLAP.rst
+++ b/doc/source/theory/DGLAP.rst
@@ -172,7 +172,7 @@ Here the strategies are:
- for ``method in ['iterate-exact', 'iterate-expanded']`` we use a discretized path-ordering :cite:`Bonvini:2012sh`:
.. math::
- \ESk{1}{a_s}{a_s^0} = \prod\limits_{k=n}^{0} \ESk{1}{a_s^{k+1}}{a_s^{k}}\quad \text{with} a_s^{n+1} = a_s
+ \ESk{1}{a_s}{a_s^0} = \prod\limits_{k=n}^{0} \ESk{1}{a_s^{k+1}}{a_s^{k}}\quad \text{with}\quad a_s^{n+1} = a_s
where the order of the product is such that later |EKO| are to the left and
@@ -522,3 +522,36 @@ evolution is simply an identity operation: e.g. for an intrinsic charm distribut
After :doc:`crossing the mass threshold ` (charm in this example) the |PDF| can not be considered intrinsic
any longer and hence, they have to be rejoined with their evolution basis elements and take then again
part in the ordinary collinear evolution.
+
+Mixed |QCD| :math:`\otimes` |QED| evolution
+-----------------------------------------
+
+For the moment in this case only the `exact` evolution is implemented.
+
+Singlet
+^^^^^^^
+
+The evolution is obtained in the same way of the pure |QCD| case, with the only difference that
+now both :math:`\gamma` and :math:`\beta_{qcd}` contain the |QED| corrections.
+
+In the case in which :math:`\alpha_{em}` is running, at every step of the iteration the corresponding value
+of :math:`a_{em}(a_s)` is used.
+
+Non singlet
+^^^^^^^^^^^
+
+For the non singlet, being it diagonal, the solution is straightforward.
+When :math:`\alpha_{em}` is fixed, the terms proportional to it are just a constant in the splitting functions, and therefore
+they can be integrated directly. For example at ``order=(1,1)`` we have
+
+.. math::
+ \tilde E^{(1,1)}_{ns}(a_s \leftarrow a_s^0) &= \exp \Bigl( -\int_{\log \mu_0^2}^{\log \mu^2}d\log\mu^2 \gamma_{ns}^{(1,0)} a_s(\log\mu^2) + \gamma_{ns}^{(1,1)} a_s(\log\mu^2) a_{em} + \gamma_{ns}^{(0,1)} a_em \Bigr) \\
+ & = \exp \Bigl( \int_{a_s^0}^{a_s}da_s\frac{\gamma_{ns}^{(1,0)} a_s + \gamma_{ns}^{(1,1)} a_s a_{em} + \gamma_{ns}^{(0,1)} a_em}{a_s^2(\beta_0 + \beta_0^{mix} a_{em})} -\int_{\log \mu_0^2}^{\log \mu^2}d\log\mu^2 \gamma_{ns}^{(0,1)} a_em\Bigr)
+
+In the last expression, the first term can be integrated with the :math:`j^{(n,m)` functions, while the second term is trivial.
+
+In the case of :math:`\alpha_{em}` running, the :math:`a_s` integration integral is divided in steps, such that in every step
+:math:`\alpha_{em}` is considered constant. In this way the solution will be the product of the solutions of every integration step:
+
+.. math::
+ \tilde E^{(1,1)}_{ns}(a_s \leftarrow a_s^0) = \prod\limits_{k=n}^{0} E^{(1,1)}_{ns}(a_s^{k+1} \leftarrow a_s^k, a_{em}^k)
diff --git a/doc/source/theory/Matching.rst b/doc/source/theory/Matching.rst
index 90a5449a2..3fe9806b0 100644
--- a/doc/source/theory/Matching.rst
+++ b/doc/source/theory/Matching.rst
@@ -135,3 +135,34 @@ EKO implements two different strategies to perform this operation, that can be s
We emphasize that in the backward evolution, below the threshold, the remaining high quark PDFs are always intrinsic and do not evolve anymore.
In fact, if the initial PDFs (above threshold) do contain an intrinsic contribution, this has to be evolved below the threshold otherwise momentum sum rules
can be violated.
+
+QED Matching
+------------
+
+In the QED case the matching is changed only because of the change of the evolution basis, therefore the only different part will be the basis rotation.
+In fact, the |OME| :math:`\mathbf{A}^{(n_f)}(\mu_{h}^2)` don't have |QED| corrections. The matching of the singlet sector is unchanged since it
+remains the same with respect to the |QCD| case. The same happens for the matching of the valence component. All the elements :math:`V_i` and :math:`T_i`
+are non-singlet components, therefore they are matched with :math:`A_{ns}`. In the end, the new components :math:`\Sigma_\Delta` and :math:`V_\Delta` are matched
+with :math:`A_{ns}` since they are both non-singlets.
+
+QED basis rotation
+------------------
+
+For the basis rotation we have to consider that we are using the intrinsic unified evolution basis. Here it will be discussed only the rotation to be applied
+to the sector :math:`(\Sigma, \Sigma_\Delta, T_i)`, being the rotation of the sector :math:`(V, V_\Delta, V_i)` completely equivalent.
+The rotation matrix is given by:
+
+.. math ::
+ \begin{pmatrix} \Sigma_{(n_f)} \\ \Sigma_{\Delta,(n_f)} \\ T_{i,(nf)} \end{pmatrix}^{(n_f+1)} =
+ \begin{pmatrix} 1 & 0 & 1 \\ a(n_f) & b(n_f) & c(n_f) \\ d(n_f) & e(n_f) & f(n_f) \end{pmatrix}
+ \begin{pmatrix} \Sigma_{(n_f)} \\ \Sigma_{\Delta,(n_f)} \\ h^+ \end{pmatrix}^{(n_f)}
+
+where
+
+.. math ::
+ a(n_f) & = \frac{1}{n_f}\Bigl(\frac{n_d(n_f+1)}{n_u(n_f+1)}n_u(n_f)-n_d(n_f)\Bigr) \\
+ b(n_f) & = \frac{n_f+1}{n_u(n_f+1)}\frac{n_u(n_f)}{n_f} \\
+ c(n_f) & = \begin{cases} \frac{n_d(n_f+1)}{n_u(n_f+1)} \quad \text{if $h$ is up-like}\\-1 \quad \text{if $h$ is down-like}\end{cases} \\
+ d(n_f) & = \begin{cases} &\frac{n_u(n_f)}{n_f} \quad \text{if $h$ is up-like ($n_f$=3,5)} \\ &\frac{n_d(n_f)}{n_f} \quad \text{if $h$ is down-like ($n_f$=2,4)} \end{cases} \\
+ e(n_f) & = \begin{cases} &\frac{n_u(n_f)}{n_f} \quad \text{if $h$ is up-like} \\ &-\frac{n_u(n_f)}{n_f} \quad \text{if $h$ is down-like} \end{cases} \\
+ f(n_f) & = \begin{cases} &-1\quad \text{if $h$ is $s$, $c$ ($n_f$=2,3)} \\ &-2 \quad \text{if $h$ is $b$, $t$ ($n_f$=4,5)} \end{cases}
diff --git a/doc/source/theory/pQCD.rst b/doc/source/theory/pQCD.rst
index 47f7a625f..fc0100803 100644
--- a/doc/source/theory/pQCD.rst
+++ b/doc/source/theory/pQCD.rst
@@ -129,10 +129,11 @@ Order specification
In the code ``order=tuple(int,int)`` specifies the |QCD| and |QED| perturbative orders of the splitting functions in terms
of :math:`a_s = \alpha_s/(4\pi)` and :math:`a_{em} = \alpha_{em}/(4\pi)`. The available perturbative expansions are the following:
-- ``order=(0,0)``: it is the non evolution case in which :math:`a_s` and :math:`a_{em}` are kept fixed and the splitting functions are null.
- ``order=(n,0)``: with :math:`n=1,2,3,4` correspond to the pure |QCD| evolution at |LO|, |NLO|, |NNLO| and |N3LO| in which the |QCD| splitting functions are expanded up to :math:`\mathcal{O}(a_s^n)` and the strong coupling is evolved using the n-th coefficient of the beta function, i.e. :math:`\beta_{n-1}`.
- ``order=(n,m)``; with :math:`n=1,2,3,4` and :math:`m=1,2` corresponds to the mixed |QED| :math:`\otimes` |QCD| evolution in which the splitting functions are expanded up to :math:`\mathcal{O}(a_s^na_{em}^m)`, the stromg coupling is evolved using up to the n-th coefficient of the beta function and the electromagnetic coupling is kept fixed.
+Observe that the case :math:`n=0` is not allowed, since it would correspond to the pure |QED| evolution or (if :math:`m > 0`) no evolution at all.
+
Sum Rules
---------
diff --git a/extras/evolution_integrals_qed/evolution_integrals.py b/extras/evolution_integrals_qed/evolution_integrals.py
new file mode 100644
index 000000000..f7892d550
--- /dev/null
+++ b/extras/evolution_integrals_qed/evolution_integrals.py
@@ -0,0 +1,89 @@
+@nb.njit(cache=True)
+def j02(a1, a0, beta0):
+ r""":math:`j^{(0,2)}` exact evolution integral.
+
+ .. math::
+ j^{(0,2)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s} \frac{da_s'}{\beta_0 a_s'^2}
+ = \frac{1.0 / a0 - 1.0 / as}{\beta_0}
+
+ Parameters
+ ----------
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta0 : float
+ LO beta function coefficient
+
+ Returns
+ -------
+ j02 : float
+ integral
+ """
+ return (1.0 / a0 - 1.0 / a1) / beta0
+
+
+@nb.njit(cache=True)
+def j03_exact(a1, a0, beta0, b_vec):
+ r""":math:`j^{(0,3)}` exact evolution integral.
+
+ .. math::
+ j^{(0,3)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s} \frac{da_s'}{\beta_0 a_s'^2 + \beta_1 a_s'^3}
+ = \frac{1.0 / a0 - 1.0 / as}{\beta_0 + \frac{b_1}{\beta_0} \left(\log(1 + 1 / (as b_1)) - \log(1 + 1 / (a0 b_1)\right)
+
+ Parameters
+ ----------
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
+
+ Returns
+ -------
+ j03_exact : float
+ integral
+ """
+ b1 = b_vec[1]
+ return -(1.0 / a1 - 1.0 / a0) / beta0 + b1 / beta0 * (
+ np.log(1.0 + 1.0 / (a1 * b1)) - np.log(1.0 + 1.0 / (a0 * b1))
+ )
+
+
+@nb.njit(cache=True)
+def j04_exact(a1, a0, beta0, b_vec):
+ r""":math:`j^{(0,4)}` exact evolution integral.
+
+ .. math::
+ j^{(0,4)}(a_s,a_s^0) &= \int\limits_{a_s^0}^{a_s}\!da_s'\,
+ \frac{1}{\beta_0 a_s'^2 + \beta_1 a_s'^3 + \beta_2 a_s'^4}\\
+ &= j^{(-1,0)}(a_s,a_s^0) - b_1 j^{(1,4)}(a_s,a_s^0) - b_2 j^{(2,4)}(a_s,a_s^0)
+
+ Parameters
+ ----------
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
+ beta2 : float
+ NNLO beta function coefficient
+
+ Returns
+ -------
+ j04_exact : complex
+ integral
+ """
+ b1 = b_vec[1]
+ b2 = b_vec[2]
+ return (
+ j02(a1, a0, beta0)
+ - b1 * j14_exact(a1, a0, beta0, b_vec)
+ - b2 * j24_exact(a1, a0, beta0, b_vec)
+ )
diff --git a/extras/qed_matching/Makefile b/extras/qed_matching/Makefile
new file mode 100644
index 000000000..3a2875840
--- /dev/null
+++ b/extras/qed_matching/Makefile
@@ -0,0 +1,9 @@
+all: matching_and_rotation.pdf
+
+%.pdf: %.tex
+ pdflatex $<
+
+clean:
+ rm -f *.aux
+ rm -f *.log
+ rm -f *.pdf
diff --git a/extras/qed_matching/matching_and_rotation.tex b/extras/qed_matching/matching_and_rotation.tex
new file mode 100644
index 000000000..2c5b6f04c
--- /dev/null
+++ b/extras/qed_matching/matching_and_rotation.tex
@@ -0,0 +1,141 @@
+\documentclass[a4paper,oneside]{article}
+\usepackage[utf8]{inputenc}
+\usepackage{xcolor}
+\usepackage{amsmath}
+\usepackage{amssymb}
+\usepackage{amsfonts}
+
+%\usepackage[a4paper,top=3cm,bottom=3cm,left=3cm,right=3cm]{geometry}
+\usepackage[a4paper,top=1.5cm,bottom=1.5cm,left=1.5cm,right=1.5cm]{geometry}
+
+
+\title{Matching and Basis Rotation for the Intrinsic Unified Evolution Basis}
+\author{Niccolò Laurenti}
+
+\date{}
+
+\begin{document}
+
+\maketitle
+
+In this document we will explain how the matching and the basis rotation are performed in the Intrinsic Unified Evolution Basis.
+
+\section{Matching}
+Fist of all, we have to perform the matching between the basis vectors in the two different flavor schemes, i.e.\ the $n_f$ and the $n_f+1$ flavor schemes. The matching of the gluon, light quark and heavy quark PDFs are the followings:
+\begin{align*}
+g^{(n_f+1)}&=A^S_{gg}g^{(n_f)}+A^S_{gq}\Sigma^{(n_f)}_{(n_f)}+A^S_{gH}h^{(n_f)} \\
+l^{(n_f+1)} &= A^{ns}_{qq} l^{(n_f)} + {1\over2n_f} A^S_{qg}g^{(n_f)} \quad \text{and the same for $\bar{l}$}\\
+h^{(n_f+1)} &= {1\over2} A^{S}_{Hg} g^{(n_f)}_{(n_f)}+ {1\over2} A^{ps}_{Hq} \Sigma^{(n_f)}_{(n_f)} + A_{HH}h^{(n_f)} \quad \text{and the same for $\bar{h}$}
+\end{align*}
+From the second relation we get that
+\begin{align*}
+l^{(n_f+1)}_+ &= A^{ns}_{qq} l^{(n_f)}_+ + {1\over n_f} A^S_{qg}g^{(n_f)} \\
+l^{(n_f+1)}_- &= A^{ns}_{qq} l^{(n_f)}_- \\
+\Sigma^{(n_f+1)}_{(n_f)}&=A^S_{qg}g^{(n_f)}+A^{ns}_{qq}\Sigma^{(n_f)}_{(n_f)} \\
+V^{(n_f+1)}_{(n_f)}&=A^{ns}_{qq}V^{(n_f)}_{(n_f)}
+\end{align*}
+while from the third relation we get that
+\begin{align*}
+h^{(n_f+1)}_+ &= A^{S}_{Hg} g^{(n_f)}_{(n_f)}+A^{ps}_{Hq} \Sigma^{(n_f)}_{(n_f)} + A_{HH}h^{(n_f)}_+ \\
+h^{(n_f+1)}_- &= A_{HH}h^{(n_f)}_-
+\end{align*}
+ The matching of the components $\Sigma_{\Delta(n_f)}$, $V_{\Delta(n_f)}$, $T_i$, $V_i$ are diagonal. For the $V$s this is trivial since they are composed by $l_-$. For $\Sigma_{\Delta(n_f)}$ instead: being it defined as
+ \begin{align*}
+ \Sigma^{(n_f+1)}_{\Delta(n_f)} & = \frac{n_d(n_f)}{n_u(n_f)} \sum_{i=1}^{n_u} u_{+\,i}^{(n_f+1)} - \sum_{i=1}^{n_d} d_{+\,i}^{(n_f+1)}
+ \end{align*}
+ the gluon contribution cancels, giving the relation
+ \begin{equation*}
+ \Sigma^{(n_f+1)}_{\Delta(n_f)}=A^{ns}_{qq}\Sigma^{(n_f)}_{\Delta(n_f)}
+\end{equation*}
+The same holds for the $T_i$ components.
+
+Observe that this holds up to NNLO, since that at N$^3$LO we have to consider also the pure singlet components of the light quark matching (I think that we have to add $\frac{1}{2n_f}A^{ps}_{qq}\Sigma^{nf}_{(nf)}$ to the matching of $l^{(n_f+1)}$, but I'm not 100\% sure. In this way the matching of $l_-$ remains diagonal, as it should be, and the same hold for $\Sigma_{\Delta(n_f)}$ and $T_i$).
+
+Up to second order the perturbative expansion of the matching terms is given by
+\begin{align*}
+A^S_{gg} & = 1+ a_s A^{S(1)}_{gg} + a_s^2 A^{S(2)}_{gg} \\
+A^S_{gq} & = 1+ a_s^2 A^{S(2)}_{gq} \\
+A^S_{gH} & = 1+ a_s A^{S(1)}_{gH} \\
+A^S_{qg} & = 1 \\
+A^{ns}_{qq} &= 1+ a_s^2 A^{ns(1)}_{qq} \\
+A^S_{Hg} & = 1+ a_s A^{S(1)}_{Hg} + a_s^2 A^{S(2)}_{Hg} \\
+A^{ps}_{Hq} & = 1+ a_s^2 A^{ps(2)}_{Hq} \\
+A_{HH} & = 1+ a_s A^{(2)}_{HH}
+\end{align*}
+
+\section{Basis Rotation}
+
+After the matching we have to perform the basis rotation from the basis with $\Sigma_{(n_f)}^{(n_f+1)}$, $\Sigma_{\Delta(n_f)}^{(n_f+1)}$, $h_+^{(n_f+1)}$ to the basis with $\Sigma_{(n_f+1)}^{(n_f+1)}$, $\Sigma_{\Delta(n_f+1)}^{(n_f+1)}$, $T_i^{(n_f+1)}$ (all the considerations that we will do for this basis rotation apply identically to the components $V$, $V_\Delta$, $h_-$, $V_i$). Being all the PDFs in the $n_f+1$ flavor scheme, from now on we will drop the superscript $(n_f+1)$.
+
+\subsection{$\Sigma$}
+For the $\Sigma$ component the basis rotation is very simple, being
+\begin{equation*}
+\Sigma_{(n_f+1)}=\Sigma_{(n_f)}+h_+
+\end{equation*}
+\subsection{$\Sigma_{\Delta}$}
+This component requires a bit more work: starting from
+\begin{equation*}
+\begin{cases}
+\Sigma &= \Sigma_u+\Sigma_d \\
+\Sigma_{\Delta} &= \frac{n_d}{n_u}\Sigma_u-\Sigma_d
+\end{cases}
+\end{equation*}
+we obtain that
+\begin{equation*}
+\begin{cases}
+\Sigma_u &= \frac{n_u}{n_f}\Bigl(\Sigma+\Sigma_{\Delta} \Bigr)\\
+\Sigma_d &= \frac{n_d}{n_f}\Sigma-\frac{n_u}{n_f}\Sigma_{\Delta}
+\end{cases}
+\end{equation*}
+
+Therefore, we find
+\begin{align*}
+\Sigma_{\Delta(n_f+1)} &= \frac{n_d(n_f+1)}{n_u(n_f+1)}\Sigma_{u(n_f)} - \Sigma_{d(n_f)} + k(n_f) h_+
+\end{align*}
+where
+\begin{equation*}
+k(n_f) =
+\begin{cases}
+\frac{n_d(n_f+1)}{n_u(n_f+1)} \quad \text{if h=up-like}\\
+-1 \quad \text{if h=down-like}
+\end{cases}
+\end{equation*}
+
+In the end we find that
+\begin{align*}
+\Sigma_{\Delta(n_f+1)} &= \frac{1}{n_f}\Bigl(\frac{n_d(n_f+1)}{n_u(n_f+1)}n_u(n_f)-n_d(n_f)\Bigr)\Sigma_{(n_f)} + \frac{n_f+1}{n_u(n_f+1)}\frac{n_u(n_f)}{n_f}\Sigma_{\Delta(n_f)} + k(n_f) h_+
+\end{align*}
+
+\subsection{$T_i$}
+In the end, we have to find the rotation for the $T_i$ component: being
+\begin{align*}
+T_3^d &=d^+ - s^+ = \Sigma_{d(n_f)} - h_+\quad \text{for $n_f=3$}\\
+T_3^u &=u^+ - c^+ =\Sigma_{u(n_f)} - h_+\quad \text{for $n_f=4$}\\
+T_8^d &=d^+ + s^+ - 2b^+ =\Sigma_{d(n_f)} - 2h_+\quad \text{for $n_f=5$}\\
+T_8^u &=u^+ + c^+ - 2t^+ =\Sigma_{u(n_f)} - 2h_+\quad \text{for $n_f=6$}
+\end{align*}
+
+Using the expressions of $\Sigma_u$ and $\Sigma_d$ as a function of $\Sigma$ and $\Sigma_\Delta$, we can write that
+\begin{equation*}
+T_i = f_1(n_f) \Sigma_{(n_f)} + f_2(n_f)\Sigma_{\Delta(n_f)} + f_3(n_f)h_+
+\end{equation*}
+with
+\begin{align*}
+f_1(n_f) &=
+\begin{cases}
+&\frac{n_u(n_f)}{n_f} \quad \text{if $h$ is up-like ($n_f$=3,5)} \\
+&\frac{n_d(n_f)}{n_f} \quad \text{if $h$ is down-like ($n_f$=2,4)}
+\end{cases} \\
+f_2(n_f) &=
+\begin{cases}
+&\frac{n_u(n_f)}{n_f} \quad \text{if $h$ is up-like} \\
+&-\frac{n_u(n_f)}{n_f} \quad \text{if $h$ is down-like}
+\end{cases} \\
+f_3(n_f) &=
+\begin{cases}
+&-1\quad \text{if $h$ is $s$, $c$ ($n_f$=2,3)} \\
+&-2 \quad \text{if $h$ is $b$, $t$ ($n_f$=4,5)}
+\end{cases}
+\end{align*}
+
+\end{document}
diff --git a/extras/uni-dglap/uni-dglap.tex b/extras/uni-dglap/uni-dglap.tex
index a5c82d077..51f807b31 100644
--- a/extras/uni-dglap/uni-dglap.tex
+++ b/extras/uni-dglap/uni-dglap.tex
@@ -28,9 +28,9 @@
g & \\
\gamma & \\
\Sigma &= \Sigma_u + \Sigma_d \\
-\Delta_\Sigma & = \frac{n_d}{n_u}\Sigma_u - \Sigma_d \\
+\Sigma_\Delta & = \frac{n_d}{n_u}\Sigma_u - \Sigma_d \\
V & = V_u + V_d \\
-\Delta_V & = \frac{n_d}{n_u}V_u - V_d \\
+V_\Delta & = \frac{n_d}{n_u}V_u - V_d \\
T_3^d &=d^+ - s^+ \\
V_3^d &=d^- - s^- \\
T_3^u &=u^+ - c^+ \\
@@ -57,7 +57,7 @@
g \\
\gamma \\
\Sigma \\
-\Delta_\Sigma
+\Sigma_\Delta
\end{pmatrix}
=
\begin{pmatrix}
@@ -70,7 +70,7 @@
g \\
\gamma \\
\Sigma \\
-\Delta_\Sigma
+\Sigma_\Delta
\end{pmatrix}
\end{equation*}
with
@@ -87,7 +87,7 @@
\mu^2\frac{d}{d\mu^2}
\begin{pmatrix}
V \\
-\Delta_V
+V_\Delta
\end{pmatrix}
=
\begin{pmatrix}
@@ -96,7 +96,7 @@
\end{pmatrix}
\begin{pmatrix}
V \\
-\Delta_V
+V_\Delta
\end{pmatrix}
\end{equation*}
\item Decoupled sector:
@@ -117,7 +117,7 @@
g \\
\gamma \\
\Sigma \\
-\Delta_\Sigma
+\Sigma_\Delta
\end{pmatrix}
= \\
&\begin{pmatrix}
@@ -130,7 +130,7 @@
g \\
\gamma \\
\Sigma \\
-\Delta_\Sigma
+\Sigma_\Delta
\end{pmatrix}
\end{align*}
@@ -139,7 +139,7 @@
\mu^2\frac{d}{d\mu^2}
\begin{pmatrix}
V \\
-\Delta_V
+V_\Delta
\end{pmatrix}
=
\begin{pmatrix}
@@ -148,7 +148,7 @@
\end{pmatrix}
\begin{pmatrix}
V \\
-\Delta_V
+V_\Delta
\end{pmatrix}
\end{equation*}
\item Decoupled sector:
diff --git a/src/eko/basis_rotation.py b/src/eko/basis_rotation.py
index ecd9c7b82..4b972592c 100644
--- a/src/eko/basis_rotation.py
+++ b/src/eko/basis_rotation.py
@@ -1,7 +1,4 @@
-"""
-This module contains the definitions of the
-:doc:`Flavor Basis and Evolution Basis `.
-"""
+"""Contains the definitions of the :doc:`Flavor Basis and Evolution Basis `."""
import numpy as np
@@ -60,6 +57,30 @@
corresponding |PDF| : :math:`\gamma, \Sigma, g, V, V_{3}, V_{8}, V_{15}, V_{24},
V_{35}, T_{3}, T_{8}, T_{15}, T_{24}, T_{35}`
"""
+unified_evol_basis = (
+ "g",
+ "ph",
+ "S",
+ "Sdelta",
+ "V",
+ "Vdelta",
+ "Td3",
+ "Vd3",
+ "Tu3",
+ "Vu3",
+ "Td8",
+ "Vd8",
+ "Tu8",
+ "Vu8",
+)
+r"""
+Sorted elements in Unified Evolution Basis as :obj:`str`.
+
+Definition: :ref:`here `.
+
+corresponding |PDF| : :math:`g, \gamma, \Sigma, \Sigma_\Delta, V, V_\Delta, T^d_3, V^d_3,
+T^u_3, V^u_3, T^d_8, V^d_8, T^u_8, V^u_8`
+"""
evol_basis_pids = tuple(
[22, 100, 21, 200]
@@ -68,7 +89,29 @@
)
"""|pid| representation of :data:`evol_basis`."""
-non_singlet_pids_map = {"ns-": 10201, "ns+": 10101, "nsV": 10200}
+unified_evol_basis_pids = tuple(
+ [21, 22, 100, 101, 200, 201]
+ + [103 + 2, 203 + 2] # d = +2
+ + [103 + 1, 203 + 1] # u = +1
+ + [108 + 2, 208 + 2]
+ + [108 + 1, 208 + 1]
+)
+r"""
+|pid| representation of :data:`unified_evol_basis`.
+
+The notation used for the non singlet compunents is the following:
+pid_ns(u) = pid_ns + 1, pid_ns(d) = pid_ns + 2.
+"""
+
+non_singlet_pids_map = {
+ "ns-": 10201,
+ "ns+": 10101,
+ "nsV": 10200,
+ "ns-u": 10202,
+ "ns-d": 10203,
+ "ns+u": 10102,
+ "ns+d": 10103,
+}
singlet_labels = ((100, 100), (100, 21), (21, 100), (21, 21))
non_singlet_labels = (
@@ -76,7 +119,43 @@
(non_singlet_pids_map["ns+"], 0),
(non_singlet_pids_map["nsV"], 0),
)
+singlet_unified_labels = (
+ (21, 21),
+ (21, 22),
+ (21, 100),
+ (21, 101), # Sdelta = 101
+ (22, 21),
+ (22, 22),
+ (22, 100),
+ (22, 101),
+ (100, 21),
+ (100, 22),
+ (100, 100),
+ (100, 101),
+ (101, 21),
+ (101, 22),
+ (101, 100),
+ (101, 101),
+)
+valence_unified_labels = (
+ (10200, 10200),
+ (10200, 10204), # Vdelta = 10204
+ (10204, 10200),
+ (10204, 10204),
+)
+non_singlet_unified_labels = (
+ (non_singlet_pids_map["ns+d"], 0),
+ (non_singlet_pids_map["ns-d"], 0),
+ (non_singlet_pids_map["ns+u"], 0),
+ (non_singlet_pids_map["ns-u"], 0),
+)
full_labels = (*singlet_labels, *non_singlet_labels)
+full_unified_labels = (
+ *singlet_unified_labels,
+ *valence_unified_labels,
+ *non_singlet_unified_labels,
+)
+
anomalous_dimensions_basis = full_labels
r"""
Sorted elements in Anomalous Dimensions Basis as :obj:`str`.
@@ -107,6 +186,30 @@
Basis rotation matrix between :doc:`Flavor Basis and Evolution Basis `.
"""
+# Tranformation from physical basis to QCDxQED evolution basis
+rotate_flavor_to_unified_evolution = np.array(
+ [
+ [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
+ [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1],
+ [0, 1, -1, 1, -1, 1, -1, 0, -1, 1, -1, 1, -1, 1],
+ [0, -1, -1, -1, -1, -1, -1, 0, 1, 1, 1, 1, 1, 1],
+ [0, -1, 1, -1, 1, -1, 1, 0, -1, 1, -1, 1, -1, 1],
+ [0, 0, 0, 0, -1, 0, 1, 0, 1, 0, -1, 0, 0, 0],
+ [0, 0, 0, 0, 1, 0, -1, 0, 1, 0, -1, 0, 0, 0],
+ [0, 0, 0, -1, 0, 1, 0, 0, 0, 1, 0, -1, 0, 0],
+ [0, 0, 0, 1, 0, -1, 0, 0, 0, 1, 0, -1, 0, 0],
+ [0, 0, -2, 0, 1, 0, 1, 0, 1, 0, 1, 0, -2, 0],
+ [0, 0, 2, 0, -1, 0, -1, 0, 1, 0, 1, 0, -2, 0],
+ [0, -2, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, -2],
+ [0, 2, 0, -1, 0, -1, 0, 0, 0, 1, 0, 1, 0, -2],
+ ]
+)
+"""
+Basis rotation matrix between :doc:`Flavor Basis and Unified Evolution Basis `.
+"""
+
+
map_ad_to_evolution = {
(100, 100): ["S.S"],
(100, 21): ["S.g"],
@@ -132,11 +235,49 @@
Map anomalous dimension sectors' names to their members
"""
+map_ad_to_unified_evolution = {
+ (21, 21): ["g.g"],
+ (21, 22): ["g.ph"],
+ (21, 100): ["g.S"],
+ (21, 101): ["g.Sdelta"],
+ (22, 21): ["ph.g"],
+ (22, 22): ["ph.ph"],
+ (22, 100): ["ph.S"],
+ (22, 101): ["ph.Sdelta"],
+ (100, 21): ["S.g"],
+ (100, 22): ["S.ph"],
+ (100, 100): ["S.S"],
+ (100, 101): ["S.Sdelta"],
+ (101, 21): ["Sdelta.g"],
+ (101, 22): ["Sdelta.ph"],
+ (101, 100): ["Sdelta.S"],
+ (101, 101): ["Sdelta.Sdelta"],
+ (10200, 10200): ["V.V"],
+ (10200, 10204): ["V.Vdelta"],
+ (10204, 10200): ["Vdelta.V"],
+ (10204, 10204): ["Vdelta.Vdelta"],
+ (non_singlet_pids_map["ns+u"], 0): [
+ "Tu3.Tu3",
+ "Tu8.Tu8",
+ ],
+ (non_singlet_pids_map["ns+d"], 0): [
+ "Td3.Td3",
+ "Td8.Td8",
+ ],
+ (non_singlet_pids_map["ns-u"], 0): [
+ "Vu3.Vu3",
+ "Vu8.Vu8",
+ ],
+ (non_singlet_pids_map["ns-d"], 0): [
+ "Vd3.Vd3",
+ "Vd8.Vd8",
+ ],
+}
+
-def ad_projector(ad_lab, nf):
+def ad_projector(ad_lab, nf, qed=False):
"""
- Build a projector (as a numpy array) for the given anomalous dimension
- sector.
+ Build a projector (as a numpy array) for the given anomalous dimension sector.
Parameters
----------
@@ -144,25 +285,35 @@ def ad_projector(ad_lab, nf):
name of anomalous dimension sector
nf : int
number of light flavors
+ qed : bool
+ activate qed
Returns
-------
proj : np.ndarray
projector over the specified sector
"""
- proj = np.zeros_like(rotate_flavor_to_evolution, dtype=float)
- l = map_ad_to_evolution[ad_lab]
- # restrict the evolution basis to light flavors
- # NOTE: the cut is only needed for "NS_p" and "NS_m", but the other lists
- # are 1-long so they are unaffected
- l = l[: (nf - 1)]
-
- for el in l:
+ if not qed:
+ proj = np.zeros_like(rotate_flavor_to_evolution, dtype=float)
+ # restrict the evolution basis to light flavors
+ # NOTE: the cut is only needed for "NS_p" and "NS_m", but the other lists
+ # are 1-long so they are unaffected
+ sector = map_ad_to_evolution[ad_lab][: (nf - 1)]
+ basis = evol_basis
+ rotate = rotate_flavor_to_evolution.copy()
+ else:
+ proj = np.zeros_like(rotate_flavor_to_unified_evolution, dtype=float)
+ # restrict the evolution basis to light flavors
+ sector = select_light_flavors_uni_ev(ad_lab, nf)
+ basis = unified_evol_basis
+ rotate = rotate_flavor_to_unified_evolution.copy()
+
+ for el in sector:
out_name, in_name = el.split(".")
- out_idx = evol_basis.index(out_name)
- in_idx = evol_basis.index(in_name)
- out = rotate_flavor_to_evolution[out_idx].copy()
- in_ = rotate_flavor_to_evolution[in_idx].copy()
+ out_idx = basis.index(out_name)
+ in_idx = basis.index(in_name)
+ out = rotate[out_idx].copy()
+ in_ = rotate[in_idx].copy()
out[: 1 + (6 - nf)] = out[len(out) - (6 - nf) :] = 0
in_[: 1 + (6 - nf)] = in_[len(in_) - (6 - nf) :] = 0
proj += (out[:, np.newaxis] * in_) / (out @ out)
@@ -170,15 +321,50 @@ def ad_projector(ad_lab, nf):
return proj
-def ad_projectors(nf):
+def select_light_flavors_uni_ev(ad_lab, nf):
+ """
+ Select light flavors for a given ad_lab.
+
+ Parameters
+ ----------
+ ad_lab : str
+ name of anomalous dimension sector
+ nf : int
+ number of light flavors
+
+ Returns
+ -------
+ sector : list
+ list of sector for a given ad_lab
+ """
+ if ad_lab[0] in [non_singlet_pids_map["ns+d"], non_singlet_pids_map["ns-d"]]:
+ if nf < 5:
+ # return only Td3 or Vd3
+ return [map_ad_to_unified_evolution[ad_lab][0]]
+ else:
+ return map_ad_to_unified_evolution[ad_lab]
+ elif ad_lab[0] in [non_singlet_pids_map["ns+u"], non_singlet_pids_map["ns-u"]]:
+ if nf < 4:
+ return []
+ elif nf < 6 and nf >= 4:
+ # return only Td3 or Vd3
+ return [map_ad_to_unified_evolution[ad_lab][0]]
+ else:
+ return map_ad_to_unified_evolution[ad_lab]
+ else:
+ return map_ad_to_evolution[ad_lab]
+
+
+def ad_projectors(nf, qed=False):
"""
- Build projectors tensor (as a numpy array), collecting all the individual
- sector projectors.
+ Build projectors tensor (as a numpy array), collecting all the individual sector projectors.
Parameters
----------
nf : int
number of light flavors
+ qed : bool
+ activate qed
Returns
-------
@@ -187,7 +373,7 @@ def ad_projectors(nf):
"""
projs = []
for ad in anomalous_dimensions_basis:
- projs.append(ad_projector(ad, nf))
+ projs.append(ad_projector(ad, nf, qed))
return np.array(projs)
diff --git a/src/eko/beta.py b/src/eko/beta.py
index 92212bf58..aebf803bf 100644
--- a/src/eko/beta.py
+++ b/src/eko/beta.py
@@ -1,18 +1,19 @@
r"""
-This module contains the QCD beta function coefficients.
+Compute the QCD beta function coefficients.
See :doc:`pQCD ingredients `.
"""
import numba as nb
-from . import constants
from ekore.harmonics.constants import zeta3
+from . import constants
+
@nb.njit(cache=True)
def beta_qcd_as2(nf):
- """Computes the first coefficient of the QCD beta function.
+ r"""Compute the first coefficient of the QCD beta function.
Implements Eq. (3.1) of :cite:`Herzog:2017ohr`.
@@ -33,7 +34,7 @@ def beta_qcd_as2(nf):
@nb.njit(cache=True)
def beta_qed_aem2(nf):
- """Computes the first coefficient of the QED beta function.
+ r"""Compute the first coefficient of the QED beta function.
Implements Eq. (7) of :cite:`Surguladze:1996hx`.
@@ -50,13 +51,16 @@ def beta_qed_aem2(nf):
"""
nu = constants.uplike_flavors(nf)
nd = nf - nu
- beta_qed_aem2 = -4.0 / 3 * constants.NC * (nu * constants.eu2 + nd * constants.ed2)
+ nl = 3 # TODO : pass nl as an argument??
+ beta_qed_aem2 = (
+ -4.0 / 3 * (nl + constants.NC * (nu * constants.eu2 + nd * constants.ed2))
+ )
return beta_qed_aem2
@nb.njit(cache=True)
def beta_qcd_as3(nf):
- """Computes the second coefficient of the QCD beta function.
+ r"""Compute the second coefficient of the QCD beta function.
Implements Eq. (3.2) of :cite:`Herzog:2017ohr`.
@@ -81,7 +85,7 @@ def beta_qcd_as3(nf):
@nb.njit(cache=True)
def beta_qed_aem3(nf):
- """Computes the second coefficient of the QED beta function.
+ r"""Compute the second coefficient of the QED beta function.
Implements Eq. (7) of :cite:`Surguladze:1996hx`.
@@ -98,15 +102,16 @@ def beta_qed_aem3(nf):
"""
nu = constants.uplike_flavors(nf)
nd = nf - nu
- beta_qed_aem3 = (
- -4.0 * constants.NC * (nu * constants.eu2**2 + nd * constants.ed2**2)
+ nl = 3 # TODO : pass nl as an argument??
+ beta_qed_aem3 = -4.0 * (
+ nl + constants.NC * (nu * constants.eu2**2 + nd * constants.ed2**2)
)
return beta_qed_aem3
@nb.njit(cache=True)
def beta_qcd_as2aem1(nf):
- """Computes the first QED correction of the QCD beta function.
+ r"""Compute the first QED correction of the QCD beta function.
Implements Eq. (7) of :cite:`Surguladze:1996hx`.
@@ -129,7 +134,7 @@ def beta_qcd_as2aem1(nf):
@nb.njit(cache=True)
def beta_qed_aem2as1(nf):
- """Computes the first QCD correction of the QED beta function.
+ r"""Compute the first QCD correction of the QED beta function.
Implements Eq. (7) of :cite:`Surguladze:1996hx`.
@@ -154,7 +159,7 @@ def beta_qed_aem2as1(nf):
@nb.njit(cache=True)
def beta_qcd_as4(nf):
- """Computes the third coefficient of the QCD beta function
+ r"""Compute the third coefficient of the QCD beta function.
Implements Eq. (3.3) of :cite:`Herzog:2017ohr`.
@@ -183,7 +188,7 @@ def beta_qcd_as4(nf):
@nb.njit(cache=True)
def beta_qcd_as5(nf):
- """Computes the fourth coefficient of the QCD beta function
+ r"""Compute the fourth coefficient of the QCD beta function.
Implements Eq. (3.6) of :cite:`Herzog:2017ohr`.
@@ -210,7 +215,7 @@ def beta_qcd_as5(nf):
@nb.njit(cache=True)
def beta_qcd(k, nf):
- """Compute value of a beta_qcd coefficients
+ r"""Compute value of a beta_qcd coefficients.
Parameters
----------
@@ -243,7 +248,7 @@ def beta_qcd(k, nf):
@nb.njit(cache=True)
def beta_qed(k, nf):
- """Compute value of a beta_qed coefficients
+ r"""Compute value of a beta_qed coefficients.
Parameters
----------
@@ -272,7 +277,7 @@ def beta_qed(k, nf):
@nb.njit(cache=True)
def b_qcd(k, nf):
- """Compute b_qcd coefficient.
+ r"""Compute b_qcd coefficient.
Parameters
----------
@@ -292,7 +297,7 @@ def b_qcd(k, nf):
@nb.njit(cache=True)
def b_qed(k, nf):
- """Compute b_qed coefficient.
+ r"""Compute b_qed coefficient.
Parameters
----------
diff --git a/src/eko/constants.py b/src/eko/constants.py
index b5cfb54f7..5c8f2b7ec 100644
--- a/src/eko/constants.py
+++ b/src/eko/constants.py
@@ -1,4 +1,4 @@
-"""This files sets the physical constants."""
+"""Sets the physical constants."""
import numba as nb
@@ -31,7 +31,7 @@
def update_colors(nc):
- """Updates the number of colors to :math:`NC = nc`.
+ """Update the number of colors to :math:`NC = nc`.
The Casimirs for a generic value of :math:`NC` are consistenly updated as
well.
@@ -51,19 +51,45 @@ def update_colors(nc):
@nb.njit(cache=True)
def uplike_flavors(nf):
- """Computes the number of up flavors
+ """Compute the number of up flavors.
Parameters
----------
- nf : int
- Number of active flavors
+ nf : int
+ Number of active flavors
Returns
-------
- nu : int
+ nu : int
"""
if nf not in range(2, 6 + 1):
raise NotImplementedError("Selected nf is not implemented")
nu = nf // 2
return nu
+
+
+@nb.njit(cache=True)
+def charge_combinations(nf):
+ """
+ Compute the combination of charges.
+
+ Parameters
+ ----------
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ e2avg : float
+ vue2m : float
+ vde2m : float
+
+ """
+ nu = uplike_flavors(nf)
+ nd = nf - nu
+ e2avg = (nu * eu2 + nd * ed2) / nf
+ vue2m = nu / nf * (eu2 - ed2)
+ vde2m = nd / nf * (eu2 - ed2)
+ e2delta = vde2m - vue2m + e2avg
+ return e2avg, vue2m, vde2m, e2delta
diff --git a/src/eko/couplings.py b/src/eko/couplings.py
index 0bec26704..24ba0de13 100644
--- a/src/eko/couplings.py
+++ b/src/eko/couplings.py
@@ -8,6 +8,7 @@
"""
import logging
+from math import isnan
from typing import List
import numba as nb
@@ -205,7 +206,7 @@ def expanded_n3lo(ref, beta0, b1, b2, b3, lmu):
@nb.njit(cache=True)
-def expanded_qcd(ref, order, nf, lmu):
+def expanded_qcd(ref, order, beta0, b_vec, lmu):
r"""Compute QCD expanded solution at a given order.
Parameters
@@ -214,8 +215,10 @@ def expanded_qcd(ref, order, nf, lmu):
reference value of the strong coupling
order : int
QCD order
- nf : int
- number of flavors
+ beta0 : float
+ first coefficient of the beta function
+ b_vec : list
+ list of b function coefficients (including b0)
lmu : float
logarithm of the ratio between target and reference scales
@@ -225,38 +228,30 @@ def expanded_qcd(ref, order, nf, lmu):
strong coupling at target scale :math:`a_s(\mu_R^2)`
"""
res_as = ref
- if order >= 1:
- beta_qcd0 = beta_qcd((2, 0), nf)
- # QCD LO
- as_LO = exact_lo(ref, beta_qcd0, lmu)
- res_as = as_LO
+ # LO
+ if order == 1:
+ res_as = exact_lo(ref, beta0, lmu)
# NLO
- if order >= 2:
- b_qcd1 = b_qcd((3, 0), nf)
- as_NLO = expanded_nlo(ref, beta_qcd0, b_qcd1, lmu)
- res_as = as_NLO
+ if order == 2:
+ res_as = expanded_nlo(ref, beta0, b_vec[1], lmu)
# NNLO
- if order >= 3:
- b_qcd2 = b_qcd((4, 0), nf)
- as_NNLO = expanded_nnlo(ref, beta_qcd0, b_qcd1, b_qcd2, lmu)
- res_as = as_NNLO
+ if order == 3:
+ res_as = expanded_nnlo(ref, beta0, b_vec[1], b_vec[2], lmu)
# N3LO
- if order >= 4:
- b_qcd3 = b_qcd((5, 0), nf)
- as_N3LO = expanded_n3lo(
+ if order == 4:
+ res_as = expanded_n3lo(
ref,
- beta_qcd0,
- b_qcd1,
- b_qcd2,
- b_qcd3,
+ beta0,
+ b_vec[1],
+ b_vec[2],
+ b_vec[3],
lmu,
)
- res_as = as_N3LO
return res_as
@nb.njit(cache=True)
-def expanded_qed(ref, order, nf, lmu):
+def expanded_qed(ref, order, beta0, b_vec, lmu):
r"""Compute QED expanded solution at a given order.
Parameters
@@ -265,8 +260,10 @@ def expanded_qed(ref, order, nf, lmu):
reference value of the QED coupling
order : int
QED order
- nf : int
- number of flavors
+ beta0 : float
+ first coefficient of the beta function
+ b_vec : list
+ list of b function coefficients (including b0)
lmu : float
logarithm of the ratio between target and reference scales
@@ -276,22 +273,22 @@ def expanded_qed(ref, order, nf, lmu):
QED coupling at target scale :math:`a_em(\mu_R^2)`
"""
res_aem = ref
- if order >= 1:
- beta_qed0 = beta_qed((0, 2), nf)
- # QED LO
- aem_LO = exact_lo(ref, beta_qed0, lmu)
- res_aem = aem_LO
+ # LO
+ if order == 1:
+ res_aem = exact_lo(ref, beta0, lmu)
# NLO
- if order >= 2:
- b_qed1 = b_qed((0, 3), nf)
- aem_NLO = expanded_nlo(ref, beta_qed0, b_qed1, lmu)
- res_aem = aem_NLO
+ if order == 2:
+ res_aem = expanded_nlo(ref, beta0, b_vec[1], lmu)
return res_aem
@nb.njit(cache=True)
-def couplings_expanded(order, couplings_ref, nf, scale_from, scale_to):
- r"""Compute expanded expression.
+def couplings_expanded_alphaem_running(
+ order, couplings_ref, nf, scale_from, scale_to, decoupled_running
+):
+ r"""Compute coupled expanded expression of the couplings for running alphaem.
+
+ Implement Eqs. (17-18) from :cite:`Surguladze:1996hx`
Parameters
----------
@@ -305,6 +302,8 @@ def couplings_expanded(order, couplings_ref, nf, scale_from, scale_to):
reference scale
scale_to : float
target scale
+ decoupled_running : bool
+ whether the running of the couplings is decoupled or not
Returns
-------
@@ -313,24 +312,81 @@ def couplings_expanded(order, couplings_ref, nf, scale_from, scale_to):
"""
# common vars
lmu = np.log(scale_to / scale_from)
- res_as = expanded_qcd(couplings_ref[0], order[0], nf, lmu)
- res_aem = expanded_qed(couplings_ref[1], order[1], nf, lmu)
- if order[0] >= 2 and order[1] >= 2:
- beta_qcd0 = beta_qcd((2, 0), nf)
- beta_qed0 = beta_qed((0, 2), nf)
- res_as += (
- -couplings_ref[0] ** 2
- * b_qcd((2, 1), nf)
- * np.log(1 + beta_qcd0 * couplings_ref[1] * lmu)
- )
- res_aem += (
- -couplings_ref[1] ** 2
- * b_qed((1, 2), nf)
- * np.log(1 + beta_qed0 * couplings_ref[0] * lmu)
- )
+ beta0_qcd = beta_qcd((2, 0), nf)
+ b_vec_qcd = [b_qcd((i + 2, 0), nf) for i in range(order[0])]
+ res_as = expanded_qcd(couplings_ref[0], order[0], beta0_qcd, b_vec_qcd, lmu)
+ beta0_qed = beta_qed((0, 2), nf)
+ b_vec_qed = [b_qed((0, i + 2), nf) for i in range(order[1])]
+ res_aem = expanded_qed(couplings_ref[1], order[1], beta0_qed, b_vec_qed, lmu)
+ # if order[0] >= 1 and order[1] >= 1:
+ # order[0] is always >=1
+ if not decoupled_running:
+ if order[1] >= 1:
+ res_as += (
+ -couplings_ref[0] ** 2
+ * b_qcd((2, 1), nf)
+ * np.log(1 + beta0_qcd * couplings_ref[1] * lmu)
+ )
+ res_aem += (
+ -couplings_ref[1] ** 2
+ * b_qed((1, 2), nf)
+ * np.log(1 + beta0_qed * couplings_ref[0] * lmu)
+ )
return np.array([res_as, res_aem])
+@nb.njit(cache=True)
+def couplings_expanded_fixed_alphaem(order, couplings_ref, nf, scale_from, scale_to):
+ r"""Compute coupled expanded expression of the couplings for fixed alphaem.
+
+ Parameters
+ ----------
+ order : tuple(int, int)
+ perturbation order
+ couplings_ref : numpy.ndarray
+ reference alpha_s and alpha
+ nf : int
+ value of nf for computing the couplings
+ scale_from : float
+ reference scale
+ scale_to : float
+ target scale
+
+ Returns
+ -------
+ numpy.ndarray
+ couplings at target scale :math:`a(\mu_R^2)`
+ """
+ # common vars
+ lmu = np.log(scale_to / scale_from)
+ res_as = couplings_ref[0]
+ aem = couplings_ref[1]
+ beta_qcd0 = beta_qcd((2, 0), nf)
+ if order[1] >= 1:
+ beta_qcd0 += aem * beta_qcd((2, 1), nf)
+ b_vec = [beta_qcd((i + 2, 0), nf) / beta_qcd0 for i in range(order[0])]
+ # LO
+ if order[0] == 1:
+ res_as = exact_lo(couplings_ref[0], beta_qcd0, lmu)
+ # NLO
+ if order[0] == 2:
+ res_as = expanded_nlo(couplings_ref[0], beta_qcd0, b_vec[1], lmu)
+ # NNLO
+ if order[0] == 3:
+ res_as = expanded_nnlo(couplings_ref[0], beta_qcd0, b_vec[1], b_vec[2], lmu)
+ # N3LO
+ if order[0] == 4:
+ res_as = expanded_n3lo(
+ couplings_ref[0],
+ beta_qcd0,
+ b_vec[1],
+ b_vec[2],
+ b_vec[3],
+ lmu,
+ )
+ return np.array([res_as, aem])
+
+
class Couplings:
r"""Compute the strong and electromagnetic coupling constants :math:`a_s, a_{em}`.
@@ -386,7 +442,7 @@ def assert_positive(name, var):
assert_positive("alpha_s_ref", couplings.alphas.value)
assert_positive("alpha_em_ref", couplings.alphaem.value)
assert_positive("scale_ref", couplings.alphas.scale)
- if order[0] not in [0, 1, 2, 3, 4]:
+ if order[0] not in [1, 2, 3, 4]:
raise NotImplementedError("a_s beyond N3LO is not implemented")
if order[1] not in [0, 1, 2]:
raise NotImplementedError("a_em beyond NLO is not implemented")
@@ -398,6 +454,8 @@ def assert_positive(name, var):
nf_ref = couplings.num_flavs_ref
max_nf = couplings.max_num_flavs
scheme_name = hqm_scheme.name
+ self.alphaem_running = False if isnan(couplings.alphaem.scale) else True
+ self.decoupled_running = False
# create new threshold object
self.a_ref = np.array(couplings.values) / 4.0 / np.pi # convert to a_s and a_em
@@ -410,17 +468,22 @@ def assert_positive(name, var):
)
self.hqm_scheme = scheme_name
logger.info(
- "Strong Coupling: a_s(µ_R^2=%f)%s=%f=%f/(4π)\n"
- "Electromagnetic Coupling: a_em(µ_R^2=%f)%s=%f=%f/(4π)",
+ "Strong Coupling: a_s(µ_R^2=%f)%s=%f=%f/(4π)",
self.q2_ref,
f"^(nf={nf_ref})" if nf_ref else "",
self.a_ref[0],
self.a_ref[0] * 4 * np.pi,
- self.q2_ref,
- f"^(nf={nf_ref})" if nf_ref else "",
- self.a_ref[1],
- self.a_ref[1] * 4 * np.pi,
)
+ if self.order[1] > 0:
+ logger.info(
+ "Electromagnetic Coupling: a_em(µ_R^2=%f)%s=%f=%f/(4π)\nalphaem running: %r\ndecoupled running: %r",
+ self.q2_ref,
+ f"^(nf={nf_ref})" if nf_ref else "",
+ self.a_ref[1],
+ self.a_ref[1] * 4 * np.pi,
+ self.alphaem_running,
+ self.decoupled_running,
+ )
# cache
self.cache = {}
@@ -429,8 +492,48 @@ def q2_ref(self):
"""Return reference scale."""
return self.thresholds.q2_ref
- def compute_exact(self, a_ref, nf, scale_from, scale_to):
- """Compute couplings via |RGE|.
+ def unidimensional_exact(self, beta0, b_vec, u, a_ref, method, rtol):
+ """Compute single coupling via decoupled |RGE|.
+
+ Parameters
+ ----------
+ beta0 : float
+ first coefficient of the beta function
+ b_vec : list
+ list of b function coefficients (including b0)
+ u : float
+ :math:`log(scale_to / scale_from)`
+ a_ref : float
+ reference alpha_s or alpha
+ method : string
+ method for solving the RGE
+ rtol : float
+ relative acuracy of the solution
+
+ Returns
+ -------
+ float
+ coupling at target scale :math:`a(Q^2)`
+ """
+ if len(b_vec) == 1:
+ return exact_lo(a_ref, beta0, u)
+
+ def rge(_t, a, b_vec):
+ rge = -(a**2) * (np.sum([a**k * b for k, b in enumerate(b_vec)]))
+ return rge
+
+ res = scipy.integrate.solve_ivp(
+ rge,
+ (0, beta0 * u),
+ (a_ref,),
+ args=[b_vec],
+ method=method,
+ rtol=rtol,
+ )
+ return res.y[0][-1]
+
+ def compute_exact_alphaem_running(self, a_ref, nf, scale_from, scale_to):
+ """Compute couplings via |RGE| with running alphaem.
Parameters
----------
@@ -451,79 +554,48 @@ def compute_exact(self, a_ref, nf, scale_from, scale_to):
# in LO fallback to expanded, as this is the full solution
u = np.log(scale_to / scale_from)
- def unidimensional_exact(beta0, b_vec, u, a_ref, method, rtol):
- def rge(_t, a, b_vec):
- rge = -(a**2) * (np.sum([a**k * b for k, b in enumerate(b_vec)]))
- return rge
-
- res = scipy.integrate.solve_ivp(
- rge,
- (0, beta0 * u),
- (a_ref,),
- args=[b_vec],
- method=method,
- rtol=rtol,
- )
- return res.y[0][-1]
-
- if self.order in [(0, 0), (0, 1), (1, 0), (1, 1)]:
- return couplings_expanded(
- self.order, a_ref, nf, scale_from, float(scale_to)
- )
- if self.order[0] in [0, 1]:
- # return expanded solution for a_s and exact for a_em
- a_s = couplings_expanded(
+ if self.order == (1, 0):
+ return couplings_expanded_fixed_alphaem(
self.order, a_ref, nf, scale_from, float(scale_to)
- )[0]
- beta0_qed = beta_qed((0, 2), nf)
- b_qed_vec = [1.0]
- # NLO
- # if self.order[1] >= 2: # I think that at this point this if is always true
- b_qed_vec.append(b_qed((0, 3), nf))
- a_em = unidimensional_exact(
- beta0_qed, b_qed_vec, u, a_ref[1], "Radau", 1e-6
)
- return np.array([a_s, a_em])
- if self.order[1] in [0, 1]:
- # return expanded solution for a_em and exact for a_s
- a_em = couplings_expanded(
- self.order, a_ref, nf, scale_from, float(scale_to)
- )[1]
- beta0_qcd = beta_qcd((2, 0), nf)
- b_qcd_vec = [1.0]
- # NLO
- if self.order[0] >= 2:
- b_qcd_vec.append(b_qcd((3, 0), nf))
- # NNLO
- if self.order[0] >= 3:
- b_qcd_vec.append(b_qcd((4, 0), nf))
- # N3LO
- if self.order[0] >= 4:
- b_qcd_vec.append(b_qcd((5, 0), nf))
- a_s = unidimensional_exact(beta0_qcd, b_qcd_vec, u, a_ref[0], "Radau", 1e-6)
- return np.array([a_s, a_em])
- # otherwise rescale the RGE to run in terms of
- # u = ln(scale_to/scale_from)
+
beta_qcd_vec = [beta_qcd((2, 0), nf)]
beta_qcd_mix = 0
+ beta_qed_mix = 0
# NLO
if self.order[0] >= 2:
beta_qcd_vec.append(beta_qcd((3, 0), nf))
- beta_qed_mix = beta_qed((1, 2), nf)
# NNLO
if self.order[0] >= 3:
beta_qcd_vec.append(beta_qcd((4, 0), nf))
# N3LO
if self.order[0] >= 4:
beta_qcd_vec.append(beta_qcd((5, 0), nf))
- beta_qed_vec = [beta_qed((0, 2), nf)]
- beta_qed_mix = 0
- if self.order[1] >= 2:
- beta_qcd_mix = beta_qcd((2, 1), nf)
- beta_qed_vec.append(beta_qed((0, 3), nf))
+ if self.order[1] == 0:
+ b_qcd_vec = [
+ beta_qcd_vec[i] / beta_qcd_vec[0] for i in range(self.order[0])
+ ]
+ rge_qcd = self.unidimensional_exact(
+ beta_qcd_vec[0],
+ b_qcd_vec,
+ u,
+ a_ref[0],
+ method="Radau",
+ rtol=1e-6,
+ )
+ # for order = (qcd, 0) with qcd > 1 we return the exact solution for the QCD RGE
+ # while aem is constant
+ return np.array([rge_qcd, a_ref[1]])
+ if self.order[1] >= 1:
+ beta_qed_vec = [beta_qed((0, 2), nf)]
+ if not self.decoupled_running:
+ beta_qcd_mix = beta_qcd((2, 1), nf)
+ beta_qed_mix = beta_qed((1, 2), nf) # order[0] is always at least 1
+ if self.order[1] >= 2:
+ beta_qed_vec.append(beta_qed((0, 3), nf))
# integration kernel
- def rge(_t, a, beta_qcd_vec, beta_qed_vec):
+ def rge(_t, a, beta_qcd_vec, beta_qcd_mix, beta_qed_vec, beta_qed_mix):
rge_qcd = -(a[0] ** 2) * (
np.sum([a[0] ** k * b for k, b in enumerate(beta_qcd_vec)])
+ a[1] * beta_qcd_mix
@@ -540,12 +612,61 @@ def rge(_t, a, beta_qcd_vec, beta_qed_vec):
rge,
(0, u),
a_ref,
- args=[beta_qcd_vec, beta_qed_vec],
+ args=[beta_qcd_vec, beta_qcd_mix, beta_qed_vec, beta_qed_mix],
method="Radau",
rtol=1e-6,
)
return np.array([res.y[0][-1], res.y[1][-1]])
+ def compute_exact_fixed_alphaem(self, a_ref, nf, scale_from, scale_to):
+ """Compute couplings via |RGE| with fixed alphaem.
+
+ Parameters
+ ----------
+ as_ref : numpy.ndarray
+ reference alpha_s and alpha
+ nf : int
+ value of nf for computing alpha_i
+ scale_from : float
+ reference scale
+ scale_to : float
+ target scale
+
+ Returns
+ -------
+ numpy.ndarray
+ couplings at target scale :math:`a(Q^2)`
+ """
+ u = np.log(scale_to / scale_from)
+
+ if self.order in [(1, 0), (1, 1)]:
+ return couplings_expanded_fixed_alphaem(
+ self.order, a_ref, nf, scale_from, float(scale_to)
+ )
+
+ beta_vec = [beta_qcd((2, 0), nf)]
+ # NLO
+ if self.order[0] >= 2:
+ beta_vec.append(beta_qcd((3, 0), nf))
+ # NNLO
+ if self.order[0] >= 3:
+ beta_vec.append(beta_qcd((4, 0), nf))
+ # N3LO
+ if self.order[0] >= 4:
+ beta_vec.append(beta_qcd((5, 0), nf))
+ if self.order[1] >= 1:
+ beta_vec[0] += a_ref[1] * beta_qcd((2, 1), nf)
+ b_vec = [i / beta_vec[0] for i in beta_vec]
+ rge_qcd = self.unidimensional_exact(
+ beta_vec[0],
+ b_vec,
+ u,
+ a_ref[0],
+ method="Radau",
+ rtol=1e-6,
+ )
+ return np.array([rge_qcd, a_ref[1]])
+
def compute(self, a_ref, nf, scale_from, scale_to):
"""Compute actual couplings.
@@ -574,13 +695,28 @@ def compute(self, a_ref, nf, scale_from, scale_to):
except KeyError:
# at the moment everything is expanded - and type has been checked in the constructor
if self.method == "exact":
- a_new = self.compute_exact(
- a_ref.astype(float), nf, scale_from, scale_to
- )
+ if self.alphaem_running:
+ a_new = self.compute_exact_alphaem_running(
+ a_ref.astype(float), nf, scale_from, scale_to
+ )
+ else:
+ a_new = self.compute_exact_fixed_alphaem(
+ a_ref.astype(float), nf, scale_from, scale_to
+ )
else:
- a_new = couplings_expanded(
- self.order, a_ref.astype(float), nf, scale_from, float(scale_to)
- )
+ if self.alphaem_running:
+ a_new = couplings_expanded_alphaem_running(
+ self.order,
+ a_ref.astype(float),
+ nf,
+ scale_from,
+ float(scale_to),
+ self.decoupled_running,
+ )
+ else:
+ a_new = couplings_expanded_fixed_alphaem(
+ self.order, a_ref.astype(float), nf, scale_from, float(scale_to)
+ )
self.cache[key] = a_new.copy()
return a_new
@@ -598,6 +734,8 @@ def a(
final scale to evolve to :math:`\mu_R^2`
fact_scale : float
factorization scale (if different from final scale)
+ nf_to : int
+ final nf value
Returns
-------
@@ -641,7 +779,9 @@ def a(
return final_a
def a_s(self, scale_to, fact_scale=None, nf_to=None):
- r"""Compute coupling :math:`a_s(\mu_R^2) = \frac{\alpha_s(\mu_R^2)}{4\pi}`.
+ r"""Compute strong coupling.
+
+ The strong oupling uses the normalization :math:`a_s(\mu_R^2) = \frac{\alpha_s(\mu_R^2)}{4\pi}`.
Parameters
----------
@@ -649,6 +789,8 @@ def a_s(self, scale_to, fact_scale=None, nf_to=None):
final scale to evolve to :math:`\mu_R^2`
fact_scale : float
factorization scale (if different from final scale)
+ nf_to : int
+ final nf value
Returns
-------
@@ -657,6 +799,27 @@ def a_s(self, scale_to, fact_scale=None, nf_to=None):
"""
return self.a(scale_to, fact_scale, nf_to)[0]
+ def a_em(self, scale_to, fact_scale=None, nf_to=None):
+ r"""Compute electromagnetic coupling.
+
+ The electromagnetic oupling uses the normalization :math:`a_em(\mu_R^2) = \frac{\alpha_em(\mu_R^2)}{4\pi}`.
+
+ Parameters
+ ----------
+ scale_to : float
+ final scale to evolve to :math:`\mu_R^2`
+ fact_scale : float
+ factorization scale (if different from final scale)
+ nf_to : int
+ final nf value
+
+ Returns
+ -------
+ a_em : float
+ couplings :math:`a_em(\mu_R^2) = \frac{\alpha_em(\mu_R^2)}{4\pi}`
+ """
+ return self.a(scale_to, fact_scale, nf_to)[1]
+
def compute_matching_coeffs_up(mass_scheme, nf):
r"""Compute the upward matching coefficients.
diff --git a/src/eko/evolution_operator/__init__.py b/src/eko/evolution_operator/__init__.py
index ee17b9cd6..81f10ee61 100644
--- a/src/eko/evolution_operator/__init__.py
+++ b/src/eko/evolution_operator/__init__.py
@@ -21,7 +21,11 @@
from .. import interpolation, mellin
from .. import scale_variations as sv
from ..kernels import non_singlet as ns
+from ..kernels import non_singlet_qed as qed_ns
from ..kernels import singlet as s
+from ..kernels import singlet_qed as qed_s
+from ..kernels import utils
+from ..kernels import valence_qed as qed_v
from ..member import OpMember
logger = logging.getLogger(__name__)
@@ -50,8 +54,69 @@ def select_singlet_element(ker, mode0, mode1):
return ker[k, l]
+@nb.njit(cache=True)
+def select_QEDsinglet_element(ker, mode0, mode1):
+ """Select element of the QEDsinglet matrix.
+
+ Parameters
+ ----------
+ ker : numpy.ndarray
+ QEDsinglet integration kernel
+ mode0 : int
+ id for first sector element
+ mode1 : int
+ id for second sector element
+ Returns
+ -------
+ ker : complex
+ QEDsinglet integration kernel element
+ """
+ if mode0 == 21:
+ index1 = 0
+ elif mode0 == 22:
+ index1 = 1
+ elif mode0 == 100:
+ index1 = 2
+ else:
+ index1 = 3
+ if mode1 == 21:
+ index2 = 0
+ elif mode1 == 22:
+ index2 = 1
+ elif mode1 == 100:
+ index2 = 2
+ else:
+ index2 = 3
+ return ker[index1, index2]
+
+
+@nb.njit(cache=True)
+def select_QEDvalence_element(ker, mode0, mode1):
+ """
+ Select element of the QEDvalence matrix.
+
+ Parameters
+ ----------
+ ker : numpy.ndarray
+ QEDvalence integration kernel
+ mode0 : int
+ id for first sector element
+ mode1 : int
+ id for second sector element
+ Returns
+ -------
+ ker : complex
+ QEDvalence integration kernel element
+ """
+ index1 = 0 if mode0 == 10200 else 1
+ index2 = 0 if mode1 == 10200 else 1
+ return ker[index1, index2]
+
+
spec = [
("is_singlet", nb.boolean),
+ ("is_QEDsinglet", nb.boolean),
+ ("is_QEDvalence", nb.boolean),
("is_log", nb.boolean),
("logx", nb.float64),
("u", nb.float64),
@@ -76,6 +141,8 @@ class QuadKerBase:
def __init__(self, u, is_log, logx, mode0):
self.is_singlet = mode0 in [100, 21, 90]
+ self.is_QEDsinglet = mode0 in [21, 22, 100, 101, 90]
+ self.is_QEDvalence = mode0 in [10200, 10204]
self.is_log = is_log
self.u = u
self.logx = logx
@@ -83,7 +150,10 @@ def __init__(self, u, is_log, logx, mode0):
@property
def path(self):
"""Return the associated instance of :class:`eko.mellin.Path`."""
- return mellin.Path(self.u, self.logx, self.is_singlet)
+ if self.is_singlet or self.is_QEDsinglet:
+ return mellin.Path(self.u, self.logx, True)
+ else:
+ return mellin.Path(self.u, self.logx, False)
@property
def n(self):
@@ -124,8 +194,11 @@ def quad_ker(
is_log,
logx,
areas,
- as1,
- as0,
+ as_list,
+ mu2_from,
+ mu2_to,
+ a_half,
+ alphaem_running,
nf,
L,
ev_op_iterations,
@@ -159,6 +232,14 @@ def quad_ker(
target coupling value
as0 : float
initial coupling value
+ mu2_from : float
+ initial value of mu2
+ mu2_from : float
+ final value of mu2
+ aem_list : list
+ list of electromagnetic coupling values
+ alphaem_running : bool
+ whether alphaem is running or not
nf : int
number of active flavors
L : float
@@ -185,8 +266,103 @@ def quad_ker(
integrand = ker_base.integrand(areas)
if integrand == 0.0:
return 0.0
+ if order[1] == 0:
+ ker = quad_ker_qcd(
+ ker_base,
+ order,
+ mode0,
+ mode1,
+ method,
+ as_list[-1],
+ as_list[0],
+ nf,
+ L,
+ ev_op_iterations,
+ ev_op_max_order,
+ sv_mode,
+ is_threshold,
+ is_polarized,
+ is_time_like,
+ )
+ else:
+ ker = quad_ker_qed(
+ ker_base,
+ order,
+ mode0,
+ mode1,
+ method,
+ as_list,
+ mu2_from,
+ mu2_to,
+ a_half,
+ alphaem_running,
+ nf,
+ L,
+ ev_op_iterations,
+ ev_op_max_order,
+ sv_mode,
+ is_threshold,
+ )
+
+ # recombine everything
+ return np.real(ker * integrand)
+
+
+@nb.njit(cache=True)
+def quad_ker_qcd(
+ ker_base,
+ order,
+ mode0,
+ mode1,
+ method,
+ as1,
+ as0,
+ nf,
+ L,
+ ev_op_iterations,
+ ev_op_max_order,
+ sv_mode,
+ is_threshold,
+ is_polarized,
+ is_time_like,
+):
+ """Raw evolution kernel inside quad.
- # compute the actual evolution kernel
+ Parameters
+ ----------
+ quad_ker : float
+ quad argument
+ order : int
+ perturbation order
+ mode0: int
+ pid for first sector element
+ mode1 : int
+ pid for second sector element
+ method : str
+ method
+ as1 : float
+ target coupling value
+ as0 : float
+ initial coupling value
+ nf : int
+ number of active flavors
+ L : float
+ logarithm of the squared ratio of factorization and renormalization scale
+ ev_op_iterations : int
+ number of evolution steps
+ ev_op_max_order : int
+ perturbative expansion order of U
+ sv_mode: int, `enum.IntEnum`
+ scale variation mode, see `eko.scale_variations.Modes`
+ is_threshold : boolean
+ is this an itermediate threshold operator?
+
+ Returns
+ -------
+ float
+ evaluated integration kernel
+ """
+ # compute the actual evolution kernel for pure QCD
if ker_base.is_singlet:
if is_polarized:
if is_time_like:
@@ -216,7 +392,7 @@ def quad_ker(
# scale var expanded is applied on the kernel
if sv_mode == sv.Modes.expanded and not is_threshold:
ker = np.ascontiguousarray(
- sv.expanded.singlet_variation(gamma_singlet, as1, order, nf, L)
+ sv.expanded.singlet_variation(gamma_singlet, as1, order, nf, L, dim=2)
) @ np.ascontiguousarray(ker)
ker = select_singlet_element(ker, mode0, mode1)
else:
@@ -243,9 +419,153 @@ def quad_ker(
)
if sv_mode == sv.Modes.expanded and not is_threshold:
ker = sv.expanded.non_singlet_variation(gamma_ns, as1, order, nf, L) * ker
+ return ker
- # recombine everything
- return np.real(ker * integrand)
+
+@nb.njit(cache=True)
+def quad_ker_qed(
+ ker_base,
+ order,
+ mode0,
+ mode1,
+ method,
+ as_list,
+ mu2_from,
+ mu2_to,
+ a_half,
+ alphaem_running,
+ nf,
+ L,
+ ev_op_iterations,
+ ev_op_max_order,
+ sv_mode,
+ is_threshold,
+):
+ """Raw evolution kernel inside quad.
+
+ Parameters
+ ----------
+ ker_base : QuadKerBase
+ quad argument
+ order : int
+ perturbation order
+ mode0: int
+ pid for first sector element
+ mode1 : int
+ pid for second sector element
+ method : str
+ method
+ as1 : float
+ target coupling value
+ as0 : float
+ initial coupling value
+ mu2_from : float
+ initial value of mu2
+ mu2_from : float
+ final value of mu2
+ aem_list : list
+ list of electromagnetic coupling values
+ alphaem_running : bool
+ whether alphaem is running or not
+ nf : int
+ number of active flavors
+ L : float
+ logarithm of the squared ratio of factorization and renormalization scale
+ ev_op_iterations : int
+ number of evolution steps
+ ev_op_max_order : int
+ perturbative expansion order of U
+ sv_mode: int, `enum.IntEnum`
+ scale variation mode, see `eko.scale_variations.Modes`
+ is_threshold : boolean
+ is this an itermediate threshold operator?
+
+ Returns
+ -------
+ float
+ evaluated integration kernel
+ """
+ # compute the actual evolution kernel for QEDxQCD
+ if ker_base.is_QEDsinglet:
+ gamma_s = ad_us.gamma_singlet_qed(order, ker_base.n, nf)
+ # scale var exponentiated is directly applied on gamma
+ if sv_mode == sv.Modes.exponentiated:
+ gamma_s = sv.exponentiated.gamma_variation_qed(
+ gamma_s, order, nf, L, alphaem_running
+ )
+ ker = qed_s.dispatcher(
+ order,
+ method,
+ gamma_s,
+ as_list,
+ a_half,
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ )
+ # scale var expanded is applied on the kernel
+ # TODO : in this way a_half[-1][1] is the aem value computed in
+ # the middle point of the last step. Instead we want aem computed in mu2_final.
+ # However the distance between the two is very small and affects only the running aem
+ if sv_mode == sv.Modes.expanded and not is_threshold:
+ ker = np.ascontiguousarray(
+ sv.expanded.singlet_variation_qed(
+ gamma_s, as_list[-1], a_half[-1][1], alphaem_running, order, nf, L
+ )
+ ) @ np.ascontiguousarray(ker)
+ ker = select_QEDsinglet_element(ker, mode0, mode1)
+ elif ker_base.is_QEDvalence:
+ gamma_v = ad_us.gamma_valence_qed(order, ker_base.n, nf)
+ # scale var exponentiated is directly applied on gamma
+ if sv_mode == sv.Modes.exponentiated:
+ gamma_v = sv.exponentiated.gamma_variation_qed(
+ gamma_v, order, nf, L, alphaem_running
+ )
+ ker = qed_v.dispatcher(
+ order,
+ method,
+ gamma_v,
+ as_list,
+ a_half,
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ )
+ # scale var expanded is applied on the kernel
+ if sv_mode == sv.Modes.expanded and not is_threshold:
+ ker = np.ascontiguousarray(
+ sv.expanded.valence_variation_qed(
+ gamma_v, as_list[-1], a_half[-1][1], alphaem_running, order, nf, L
+ )
+ ) @ np.ascontiguousarray(ker)
+ ker = select_QEDvalence_element(ker, mode0, mode1)
+ else:
+ gamma_ns = ad_us.gamma_ns_qed(order, mode0, ker_base.n, nf)
+ # scale var exponentiated is directly applied on gamma
+ if sv_mode == sv.Modes.exponentiated:
+ gamma_ns = sv.exponentiated.gamma_variation_qed(
+ gamma_ns, order, nf, L, alphaem_running
+ )
+ ker = qed_ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ as_list,
+ a_half[:, 1],
+ alphaem_running,
+ nf,
+ ev_op_iterations,
+ mu2_from,
+ mu2_to,
+ )
+ if sv_mode == sv.Modes.expanded and not is_threshold:
+ ker = (
+ sv.expanded.non_singlet_variation_qed(
+ gamma_ns, as_list[-1], a_half[-1][1], alphaem_running, order, nf, L
+ )
+ * ker
+ )
+ return ker
class Operator(sv.ModeMixin):
@@ -274,6 +594,7 @@ class Operator(sv.ModeMixin):
log_label = "Evolution"
# complete list of possible evolution operators labels
full_labels = br.full_labels
+ full_labels_qed = br.full_unified_labels
def __init__(
self, config, managers, nf, q2_from, q2_to, mellin_cut=5e-2, is_threshold=False
@@ -288,6 +609,10 @@ def __init__(
self.is_threshold = is_threshold
self.op_members = {}
self.order = tuple(config["order"])
+ self.alphaem_running = self.managers["couplings"].alphaem_running
+ if self.log_label == "Evolution":
+ self.a = self.compute_a()
+ self.compute_aem_list()
@property
def n_pools(self):
@@ -330,22 +655,82 @@ def sv_exponentiated_shift(self, q2):
return q2 / self.xif2
return q2
- @property
- def a_s(self):
- """Return the computed values for :math:`a_s`."""
- sc = self.managers["strong_coupling"]
- a0 = sc.a_s(
+ def compute_a(self):
+ """Return the computed values for :math:`a_s` and :math:`a_{em}`."""
+ coupling = self.managers["couplings"]
+ a0 = coupling.a(
self.sv_exponentiated_shift(self.q2_from),
fact_scale=self.q2_from,
nf_to=self.nf,
)
- a1 = sc.a_s(
+ a1 = coupling.a(
self.sv_exponentiated_shift(self.q2_to),
fact_scale=self.q2_to,
nf_to=self.nf,
)
return (a0, a1)
+ @property
+ def a_s(self):
+ """Return the computed values for :math:`a_s`."""
+ return (self.a[0][0], self.a[1][0])
+
+ @property
+ def a_em(self):
+ """Return the computed values for :math:`a_{em}`."""
+ return (self.a[0][1], self.a[1][1])
+
+ def compute_aem_list(self):
+ """
+ Return the list of the couplings for the different values of :math:`a_s`.
+
+ This functions is needed in order to compute the values of :math:`a_s`
+ and :math:`a_em` in the middle point of the :math:`mu^2` interval, and
+ the values of :math:`a_s` at the borders of every intervals.
+ This is needed in the running_alphaem solution.
+
+ """
+ ev_op_iterations = self.config["ev_op_iterations"]
+ if self.order[1] == 0:
+ self.as_list = np.array([self.a_s[0], self.a_s[1]])
+ self.a_half_list = np.zeros((ev_op_iterations, 2))
+ else:
+ as0 = self.a_s[0]
+ as1 = self.a_s[1]
+ aem0 = self.a_em[0]
+ aem1 = self.a_em[1]
+ q2ref = self.managers["couplings"].q2_ref
+ delta_from = abs(self.q2_from - q2ref)
+ delta_to = abs(self.q2_to - q2ref)
+ # I compute the values in aem_list starting from the mu2
+ # that is closer to mu_ref.
+ if delta_from > delta_to:
+ a_start = np.array([as1, aem1])
+ mu2_start = self.q2_to
+ else:
+ a_start = np.array([as0, aem0])
+ mu2_start = self.q2_from
+ couplings = self.managers["couplings"]
+ mu2_steps = utils.geomspace(self.q2_from, self.q2_to, 1 + ev_op_iterations)
+ mu2_l = mu2_steps[0]
+ self.as_list = np.array(
+ [
+ couplings.compute(
+ a_ref=a_start, nf=self.nf, scale_from=mu2_start, scale_to=mu2
+ )[0]
+ for mu2 in mu2_steps
+ ]
+ )
+ a_half = np.zeros((ev_op_iterations, 2))
+ for step, mu2_h in enumerate(mu2_steps[1:]):
+ mu2_half = (mu2_h + mu2_l) / 2.0
+ a_s, aem = couplings.compute(
+ a_ref=a_start, nf=self.nf, scale_from=mu2_start, scale_to=mu2_half
+ )
+ a_half[step] = [a_s, aem]
+ mu2_l = mu2_h
+ self.a_half_list = a_half
+
@property
def labels(self):
"""Compute necessary sector labels to compute.
@@ -357,20 +742,37 @@ def labels(self):
"""
labels = []
# the NS sector is dynamic
- if self.config["debug_skip_non_singlet"]:
- logger.warning("%s: skipping non-singlet sector", self.log_label)
- else:
- # add + as default
- labels.append(br.non_singlet_labels[1])
- if self.order[0] >= 2: # - becomes different starting from NLO
- labels.append(br.non_singlet_labels[0])
- if self.order[0] >= 3: # v also becomes different starting from NNLO
- labels.append(br.non_singlet_labels[2])
- # singlet sector is fixed
- if self.config["debug_skip_singlet"]:
- logger.warning("%s: skipping singlet sector", self.log_label)
+ if self.order[1] == 0:
+ if self.config["debug_skip_non_singlet"]:
+ logger.warning("%s: skipping non-singlet sector", self.log_label)
+ else:
+ # add + as default
+ labels.append(br.non_singlet_labels[1])
+ if self.order[0] >= 2: # - becomes different starting from NLO
+ labels.append(br.non_singlet_labels[0])
+ if self.order[0] >= 3: # v also becomes different starting from NNLO
+ labels.append(br.non_singlet_labels[2])
+ # singlet sector is fixed
+ if self.config["debug_skip_singlet"]:
+ logger.warning("%s: skipping singlet sector", self.log_label)
+ else:
+ labels.extend(br.singlet_labels)
else:
- labels.extend(br.singlet_labels)
+ if self.config["debug_skip_non_singlet"]:
+ logger.warning("%s: skipping non-singlet sector", self.log_label)
+ else:
+ # add +u and +d as default
+ labels.append(br.non_singlet_unified_labels[0])
+ labels.append(br.non_singlet_unified_labels[2])
+ # -u and -d become different starting from O(as1aem1) or O(aem2)
+ if self.order[1] >= 2 or self.order[0] >= 1:
+ labels.append(br.non_singlet_unified_labels[1])
+ labels.append(br.non_singlet_unified_labels[3])
+ labels.extend(br.valence_unified_labels)
+ if self.config["debug_skip_singlet"]:
+ logger.warning("%s: skipping singlet sector", self.log_label)
+ else:
+ labels.extend(br.singlet_unified_labels)
return labels
def quad_ker(self, label, logx, areas):
@@ -400,8 +802,11 @@ def quad_ker(self, label, logx, areas):
is_log=self.int_disp.log,
logx=logx,
areas=areas,
- as1=self.a_s[1],
- as0=self.a_s[0],
+ as_list=self.as_list,
+ mu2_from=self.q2_from,
+ mu2_to=self.q2_to,
+ a_half=self.a_half_list,
+ alphaem_running=self.alphaem_running,
nf=self.nf,
L=np.log(self.xif2),
ev_op_iterations=self.config["ev_op_iterations"],
@@ -418,10 +823,16 @@ def initialize_op_members(self):
np.eye(self.grid_size), np.zeros((self.grid_size, self.grid_size))
)
zero = OpMember(*[np.zeros((self.grid_size, self.grid_size))] * 2)
- for n in self.full_labels:
+ if self.order[1] == 0:
+ full_labels = self.full_labels
+ non_singlet_labels = br.non_singlet_labels
+ else:
+ full_labels = self.full_labels_qed
+ non_singlet_labels = br.non_singlet_unified_labels
+ for n in full_labels:
if n in self.labels:
# non-singlet evolution and diagonal op are identities
- if n in br.non_singlet_labels or n[0] == n[1]:
+ if n in non_singlet_labels or n[0] == n[1]:
self.op_members[n] = eye.copy()
else:
self.op_members[n] = zero.copy()
@@ -455,7 +866,9 @@ def run_op_integration(
# iterate sectors
for label in self.labels:
res = integrate.quad(
- self.quad_ker(label, logx, bf.areas_representation),
+ self.quad_ker(
+ label=label, logx=logx, areas=bf.areas_representation
+ ),
0.5,
1.0 - self._mellin_cut,
epsabs=1e-12,
@@ -513,6 +926,13 @@ def compute(self):
logger.info(
"%s: a_s distance: %e -> %e", self.log_label, self.a_s[0], self.a_s[1]
)
+ if self.order[1] > 0:
+ logger.info(
+ "%s: a_em distance: %e -> %e",
+ self.log_label,
+ self.a_em[0],
+ self.a_em[1],
+ )
logger.info(
"%s: order: (%d, %d), solution strategy: %s",
self.log_label,
@@ -556,22 +976,44 @@ def integrate(
def copy_ns_ops(self):
"""Copy non-singlet kernels, if necessary."""
- if self.order[0] == 1: # in LO +=-=v
- for label in ["nsV", "ns-"]:
+ if self.order[1] == 0:
+ if self.order[0] == 1: # in LO +=-=v
+ for label in ["nsV", "ns-"]:
+ self.op_members[
+ (br.non_singlet_pids_map[label], 0)
+ ].value = self.op_members[
+ (br.non_singlet_pids_map["ns+"], 0)
+ ].value.copy()
+ self.op_members[
+ (br.non_singlet_pids_map[label], 0)
+ ].error = self.op_members[
+ (br.non_singlet_pids_map["ns+"], 0)
+ ].error.copy()
+ elif self.order[0] == 2: # in NLO -=v
self.op_members[
- (br.non_singlet_pids_map[label], 0)
+ (br.non_singlet_pids_map["nsV"], 0)
].value = self.op_members[
- (br.non_singlet_pids_map["ns+"], 0)
+ (br.non_singlet_pids_map["ns-"], 0)
].value.copy()
self.op_members[
- (br.non_singlet_pids_map[label], 0)
+ (br.non_singlet_pids_map["nsV"], 0)
].error = self.op_members[
- (br.non_singlet_pids_map["ns+"], 0)
+ (br.non_singlet_pids_map["ns-"], 0)
].error.copy()
- elif self.order[0] == 2: # in NLO -=v
- self.op_members[
- (br.non_singlet_pids_map["nsV"], 0)
- ].value = self.op_members[(br.non_singlet_pids_map["ns-"], 0)].value.copy()
- self.op_members[
- (br.non_singlet_pids_map["nsV"], 0)
- ].error = self.op_members[(br.non_singlet_pids_map["ns-"], 0)].error.copy()
+ # at O(as0aem1) u-=u+, d-=d+
+ # starting from O(as1aem1) P+ != P-
+ # However the solution with pure QED is not implemented in EKO
+ # so the ns anomalous dimensions are always different
+ # elif self.order[1] == 1 and self.order[0] == 0:
+ # self.op_members[
+ # (br.non_singlet_pids_map["ns-u"], 0)
+ # ].value = self.op_members[(br.non_singlet_pids_map["ns+u"], 0)].value.copy()
+ # self.op_members[
+ # (br.non_singlet_pids_map["ns-u"], 0)
+ # ].error = self.op_members[(br.non_singlet_pids_map["ns+u"], 0)].error.copy()
+ # self.op_members[
+ # (br.non_singlet_pids_map["ns-d"], 0)
+ # ].value = self.op_members[(br.non_singlet_pids_map["ns+d"], 0)].value.copy()
+ # self.op_members[
+ # (br.non_singlet_pids_map["ns-d"], 0)
+ # ].error = self.op_members[(br.non_singlet_pids_map["ns+d"], 0)].error.copy()
diff --git a/src/eko/evolution_operator/flavors.py b/src/eko/evolution_operator/flavors.py
index abd39fcc3..016292160 100644
--- a/src/eko/evolution_operator/flavors.py
+++ b/src/eko/evolution_operator/flavors.py
@@ -1,40 +1,37 @@
-r"""
-The write-up of the matching conditions is given in
-:doc:`Matching Conditions `.
-
-"""
+r"""The write-up of the matching conditions is given in :doc:`Matching Conditions `."""
import numpy as np
from .. import basis_rotation as br
+from .. import constants
def pids_from_intrinsic_evol(label, nf, normalize):
- r"""
- Obtain the list of pids with their corresponding weight, that are contributing to ``evol``
+ r"""Obtain the list of pids with their corresponding weight, that are contributing to ``evol``.
The normalization of the weights is only needed for the output rotation:
- if we want to build e.g. the singlet in the initial state we simply have to sum
- to obtain :math:`S = u + \bar u + d + \bar d + \ldots`
+ to obtain :math:`\Sigma = u + \bar u + d + \bar d + \ldots`
- if we want to rotate back in the output we have to *normalize* the weights:
- e.g. in nf=3 :math:`u = \frac 1 6 S + \frac 1 6 V + \ldots`
+ e.g. in nf=3 :math:`u = \frac 1 6 \Sigma + \frac 1 6 V + \ldots`
The normalization can only happen here since we're actively cutting out some
flavor (according to ``nf``).
Parameters
----------
- evol : str
- evolution label
- nf : int
- maximum number of light flavors
- normalize : bool
- normalize output
+ label : str
+ evolution label
+ nf : int
+ maximum number of light flavors
+ normalize : bool
+ normalize output
Returns
-------
- m : list
+ list(float)
+ list of weights
"""
try:
evol_idx = br.evol_basis.index(label)
@@ -55,49 +52,64 @@ def pids_from_intrinsic_evol(label, nf, normalize):
return weights
-def get_range(evol_labels):
- """
- Determine the number of light and heavy flavors participating in the input and output.
+def get_range(evol_labels, qed=False):
+ """Determine the number of light and heavy flavors participating in the input and output.
Here, we assume that the T distributions (e.g. T15) appears *always*
before the corresponding V distribution (e.g. V15).
+ Parameters
+ ----------
+ qed : bool
+ activate qed
+
Returns
-------
- nf_in : int
- number of light flavors in the input
- nf_out : int
- number of light flavors in the output
+ nf_in : int
+ number of light flavors in the input
+ nf_out : int
+ number of light flavors in the output
"""
nf_in = 3
nf_out = 3
- def update(label):
+ def update(label, qed=False):
nf = 3
if label[0] == "T":
- nf = round(np.sqrt(int(label[1:]) + 1))
+ if not qed:
+ nf = round(np.sqrt(int(label[1:]) + 1))
+ else:
+ if label[1:] == "d3":
+ nf = 3
+ elif label[1:] == "u3":
+ nf = 4
+ elif label[1:] == "d8":
+ nf = 5
+ elif label[1:] == "u8":
+ nf = 6
+ else:
+ raise ValueError(f"{label[1:]} is not possible")
return nf
for op in evol_labels:
- nf_in = max(update(op.input), nf_in)
- nf_out = max(update(op.target), nf_out)
+ nf_in = max(update(op.input, qed), nf_in)
+ nf_out = max(update(op.target, qed), nf_out)
return nf_in, nf_out
def rotate_pm_to_flavor(label):
- """
- Rotate from +- basis to flavor basis.
+ """Rotate from +- basis to flavor basis.
Parameters
----------
- label : str
- label
+ label : str
+ label
Returns
-------
- l : list(float)
- list of weights
+ list(float)
+ list of weights
"""
# g and ph are unaltered
if label in ["g", "ph"]:
@@ -117,44 +129,75 @@ def rotate_pm_to_flavor(label):
return l
-def rotate_matching(nf, inverse=False):
- """
- Rotation between matching basis (with e.g. S,g,...V8 and c+,c-) and new true evolution basis
- (with S,g,...V8,T15,V15).
+def rotate_matching(nf, qed=False, inverse=False):
+ """Rotation between matching basis (with e.g. S,g,...V8 and c+,c-) and new true evolution basis (with S,g,...V8,T15,V15).
Parameters
----------
- nf : int
- number of active flavors in the higher patch: to activate T15, nf=4
- inverse : bool
- use inverse conditions?
+ nf : int
+ number of active flavors in the higher patch: to activate T15, nf=4
+ qed : bool
+ use QED?
+ inverse : bool
+ use inverse conditions?
Returns
-------
- l : dict
- mapping in dot notation between the bases
+ dict
+ mapping in dot notation between the bases
"""
# the gluon and the photon do not care about new quarks
l = {"g.g": 1.0, "ph.ph": 1.0}
# already active distributions
- for k in range(2, nf): # nf is the upper, so excluded
- n = k**2 - 1
- l[f"V{n}.V{n}"] = 1.0
- l[f"T{n}.T{n}"] = 1.0
- # the new contributions
- n = nf**2 - 1 # nf is pointing upwards
q = br.quark_names[nf - 1]
- for (tot, oth, qpm) in (("S", f"T{n}", f"{q}+"), ("V", f"V{n}", f"{q}-")):
- if inverse:
- l[f"{tot}.{tot}"] = (nf - 1.0) / nf
- l[f"{tot}.{oth}"] = 1.0 / nf
- l[f"{qpm}.{tot}"] = 1.0 / nf
- l[f"{qpm}.{oth}"] = -1.0 / nf
- else:
- l[f"{tot}.{tot}"] = 1.0
- l[f"{tot}.{qpm}"] = 1.0
- l[f"{oth}.{tot}"] = 1.0
- l[f"{oth}.{qpm}"] = -(nf - 1.0)
+ if not qed:
+ for k in range(2, nf): # nf is the upper, so excluded
+ n = k**2 - 1
+ l[f"V{n}.V{n}"] = 1.0
+ l[f"T{n}.T{n}"] = 1.0
+ # the new contributions
+ n = nf**2 - 1 # nf is pointing upwards
+ for (tot, oth, qpm) in (("S", f"T{n}", f"{q}+"), ("V", f"V{n}", f"{q}-")):
+ if inverse:
+ l[f"{tot}.{tot}"] = (nf - 1.0) / nf
+ l[f"{tot}.{oth}"] = 1.0 / nf
+ l[f"{qpm}.{tot}"] = 1.0 / nf
+ l[f"{qpm}.{oth}"] = -1.0 / nf
+ else:
+ l[f"{tot}.{tot}"] = 1.0
+ l[f"{tot}.{qpm}"] = 1.0
+ l[f"{oth}.{tot}"] = 1.0
+ l[f"{oth}.{qpm}"] = -(nf - 1.0)
+ else:
+ names = {3: "d3", 4: "u3", 5: "d8", 6: "u8"}
+ for k in range(3, nf):
+ l[f"V{names[k]}.V{names[k]}"] = 1.0
+ l[f"T{names[k]}.T{names[k]}"] = 1.0
+ for (tot, totdelta, oth, qpm) in (
+ ("S", "Sdelta", f"T{names[nf]}", f"{q}+"),
+ ("V", "Vdelta", f"V{names[nf]}", f"{q}-"),
+ ):
+ a, b, c, d, e, f = qed_rotation_parameters(nf)
+ if inverse:
+ den = -b * d + a * e - c * e + b * f
+ l[f"{tot}.{tot}"] = -(c * e - b * f) / den
+ l[f"{tot}.{totdelta}"] = e / den
+ l[f"{tot}.{oth}"] = -b / den
+ l[f"{totdelta}.{tot}"] = (c * d - a * f) / den
+ l[f"{totdelta}.{totdelta}"] = (f - d) / den
+ l[f"{totdelta}.{oth}"] = (a - c) / den
+ l[f"{qpm}.{tot}"] = (-b * d + a * e) / den
+ l[f"{qpm}.{totdelta}"] = -e / den
+ l[f"{qpm}.{oth}"] = b / den
+ else:
+ l[f"{tot}.{tot}"] = 1.0
+ l[f"{tot}.{qpm}"] = 1.0
+ l[f"{totdelta}.{tot}"] = a
+ l[f"{totdelta}.{totdelta}"] = b
+ l[f"{totdelta}.{qpm}"] = c
+ l[f"{oth}.{tot}"] = d
+ l[f"{oth}.{totdelta}"] = e
+ l[f"{oth}.{qpm}"] = f
# also higher quarks do not care
for k in range(nf + 1, 6 + 1):
q = br.quark_names[k - 1]
@@ -163,27 +206,77 @@ def rotate_matching(nf, inverse=False):
return l
-def rotate_matching_inverse(nf):
- return rotate_matching(nf, True)
+def rotate_matching_inverse(nf, qed=False):
+ """Inverse rotation between matching basis (with e.g. S,g,...V8 and c+,c-) and new true evolution basis (with S,g,...V8,T15,V15).
+
+ Parameters
+ ----------
+ nf : int
+ number of active flavors in the higher patch: to activate T15, nf=4
+ qed : bool
+ use QED?
+
+ Returns
+ -------
+ dict
+ mapping in dot notation between the bases
+ """
+ return rotate_matching(nf, qed, True)
+
+
+def qed_rotation_parameters(nf):
+ r"""Parameters of the QED basis rotation.
+
+ From :math:`(\Sigma, \Sigma_{\Delta}, h_+)` into :math:`(\Sigma, \Sigma_{\Delta}, T_i^j)`,
+ or equivalentely for :math:`V, V_{\Delta}, V_i^j, h_-`.
+
+ Parameters
+ ----------
+ nf : int
+ number of active flavors in the higher patch: e.g. to activate :math:`T_3^u` or :math:`V_3^u` choose ``nf=4``
+
+ Returns
+ -------
+ a,b,c,d,e,f : float
+ Parameters of the rotation: :math:`\Sigma_{\Delta} = a*\Sigma + b*\Sigma_{\Delta} + c*h_+, T_i = d*\Sigma + e*\Sigma_{\Delta} + f*h_+`
+ """
+ nu_l = constants.uplike_flavors(nf - 1)
+ nd_l = (nf - 1) - nu_l
+ nu_h = constants.uplike_flavors(nf)
+ nd_h = nf - nu_h
+ a = (nd_h / nu_h * nu_l - nd_l) / (nf - 1)
+ b = nf / nu_h * nu_l / (nf - 1)
+ if nf in [4, 6]: # heavy flavor is up-like
+ c = nd_h / nu_h
+ d = nu_l / (nf - 1)
+ e = nu_l / (nf - 1)
+ elif nf in [3, 5]: # heavy flavor is down-like
+ c = -1
+ d = nd_l / (nf - 1)
+ e = -nu_l / (nf - 1)
+ if nf in [3, 4]: # s and c unlock T3d, T3u that have -h+
+ f = -1
+ elif nf in [5, 6]: # b and t unlock T8d, T8u that have -2h+
+ f = -2
+ return a, b, c, d, e, f
def pids_from_intrinsic_unified_evol(label, nf, normalize):
- r"""
- Obtain the list of pids with their corresponding weight, that are contributing to intrinsic
- unified evolution.
+ r"""Obtain the list of pids with their corresponding weight, that are contributing to intrinsic unified evolution.
Parameters
----------
- evol : str
- evolution label
- nf : int
- maximum number of light flavors
- normalize : bool
- normalize output
+ evol : str
+ evolution label
+ nf : int
+ maximum number of light flavors
+ normalize : bool
+ normalize output
Returns
-------
- m : list
+ list(float)
+ list of weights
"""
if label in ["ph", "g", "S", "V"]:
return pids_from_intrinsic_evol(label, nf, normalize)
diff --git a/src/eko/evolution_operator/grid.py b/src/eko/evolution_operator/grid.py
index 8e8fc923b..1c2c905c8 100644
--- a/src/eko/evolution_operator/grid.py
+++ b/src/eko/evolution_operator/grid.py
@@ -48,7 +48,7 @@ def __init__(
configs: Configs,
debug: Debug,
thresholds_config,
- strong_coupling,
+ couplings,
interpol_dispatcher,
):
"""Initialize `OperatorGrid`.
@@ -64,8 +64,8 @@ def __init__(
thresholds_config: eko.thresholds.ThresholdsAtlas
Instance of :class:`~eko.thresholds.Threshold` containing information about the
thresholds
- strong_coupling: eko.strong_coupling.StrongCoupling
- Instance of :class:`~eko.strong_coupling.StrongCoupling` able to generate a_s for
+ couplings: eko.couplings.StrongCoupling
+ Instance of :class:`~eko.couplings.StrongCoupling` able to generate a_s for
any q
kernel_dispatcher: eko.kernel_generation.KernelDispatcher
Instance of the :class:`~eko.kernel_generation.KernelDispatcher` with the
@@ -112,7 +112,7 @@ def __init__(
self.q2_grid = mu2grid
self.managers = dict(
thresholds_config=thresholds_config,
- strong_coupling=strong_coupling,
+ couplings=couplings,
interpol_dispatcher=interpol_dispatcher,
)
self._threshold_operators = {}
@@ -234,16 +234,14 @@ def generate(self, q2):
is_downward = is_downward_path(path)
if is_downward:
intrinsic_range = [4, 5, 6]
+ qed = self.config["order"][1] > 0
final_op = physical.PhysicalOperator.ad_to_evol_map(
- operator.op_members,
- operator.nf,
- operator.q2_to,
- intrinsic_range,
+ operator.op_members, operator.nf, operator.q2_to, intrinsic_range, qed
)
# and multiply the lower ones from the right
for op in reversed(list(thr_ops)):
phys_op = physical.PhysicalOperator.ad_to_evol_map(
- op.op_members, op.nf, op.q2_to, intrinsic_range
+ op.op_members, op.nf, op.q2_to, intrinsic_range, qed
)
# join with the basis rotation, since matching requires c+ (or likewise)
@@ -253,9 +251,10 @@ def generate(self, q2):
op.nf - 1,
op.q2_to,
intrinsic_range=intrinsic_range,
+ qed=qed,
)
invrot = member.ScalarOperator.promote_names(
- flavors.rotate_matching_inverse(op.nf), op.q2_to
+ flavors.rotate_matching_inverse(op.nf, qed), op.q2_to
)
final_op = final_op @ matching @ invrot @ phys_op
else:
@@ -264,13 +263,13 @@ def generate(self, q2):
op.nf,
op.q2_to,
intrinsic_range=intrinsic_range,
+ qed=qed,
)
rot = member.ScalarOperator.promote_names(
- flavors.rotate_matching(op.nf + 1), op.q2_to
+ flavors.rotate_matching(op.nf + 1, qed), op.q2_to
)
final_op = final_op @ rot @ matching @ phys_op
-
- values, errors = final_op.to_flavor_basis_tensor()
+ values, errors = final_op.to_flavor_basis_tensor(qed)
return {
"operator": values,
"error": errors,
diff --git a/src/eko/evolution_operator/matching_condition.py b/src/eko/evolution_operator/matching_condition.py
index 32b8cd036..059617fbf 100644
--- a/src/eko/evolution_operator/matching_condition.py
+++ b/src/eko/evolution_operator/matching_condition.py
@@ -1,6 +1,4 @@
-"""
-This module defines the matching conditions for the |VFNS| evolution.
-"""
+"""Defines the matching conditions for the |VFNS| evolution."""
from .. import basis_rotation as br
from .. import member
@@ -23,6 +21,7 @@ def split_ad_to_evol_map(
nf,
q2_thr,
intrinsic_range,
+ qed=False,
):
"""
Create the instance from the |OME|.
@@ -38,8 +37,9 @@ def split_ad_to_evol_map(
threshold value
intrinsic_range : list
list of intrinsic quark pids
+ qed : bool
+ activate qed
"""
-
m = {
"S.S": op_members[(100, 100)],
"S.g": op_members[(100, 21)],
@@ -49,10 +49,23 @@ def split_ad_to_evol_map(
}
# add elements which are already active
- for f in range(2, nf + 1):
- n = f**2 - 1
- m[f"V{n}.V{n}"] = m["V.V"]
- m[f"T{n}.T{n}"] = m["V.V"]
+ if not qed:
+ for f in range(2, nf + 1):
+ n = f**2 - 1
+ m[f"V{n}.V{n}"] = m["V.V"]
+ m[f"T{n}.T{n}"] = m["V.V"]
+ else:
+ m.update(
+ {
+ "Sdelta.Sdelta": op_members[(200, 200)],
+ "Vdelta.Vdelta": op_members[(200, 200)],
+ "ph.ph": member.OpMember.id_like(op_members[(200, 200)]).copy(),
+ }
+ )
+ names = {3: "d3", 4: "u3", 5: "d8", 6: "u8"}
+ for k in range(3, nf + 1):
+ m[f"V{names[k]}.V{names[k]}"] = m["V.V"]
+ m[f"T{names[k]}.T{names[k]}"] = m["V.V"]
# activate the next heavy quark
hq = br.quark_names[nf]
diff --git a/src/eko/evolution_operator/operator_matrix_element.py b/src/eko/evolution_operator/operator_matrix_element.py
index 3f241519a..dd0c72005 100644
--- a/src/eko/evolution_operator/operator_matrix_element.py
+++ b/src/eko/evolution_operator/operator_matrix_element.py
@@ -1,5 +1,6 @@
"""The |OME| for the non-trivial matching conditions in the |VFNS| evolution."""
+import copy
import functools
import logging
@@ -154,7 +155,7 @@ def quad_ker(
)
# compute the ome
- if ker_base.is_singlet:
+ if ker_base.is_singlet or ker_base.is_QEDsinglet:
indices = {21: 0, 100: 1, 90: 2}
if is_polarized:
if is_time_like:
@@ -227,6 +228,8 @@ class OperatorMatrixElement(Operator):
(br.matching_hminus_pid, 200),
(br.matching_hminus_pid, br.matching_hminus_pid),
]
+ # still valid in QED since Sdelta and Vdelta matchings are diagonal
+ full_labels_qed = copy.deepcopy(full_labels)
def __init__(self, config, managers, nf, q2, is_backward, L, is_msbar):
super().__init__(config, managers, nf, q2, None)
@@ -317,11 +320,11 @@ def quad_ker(self, label, logx, areas):
@property
def a_s(self):
- """Compute values for :math:`a_s`.
+ """Return the computed values for :math:`a_s`.
Note that here you need to use :math:`a_s^{n_f+1}`
"""
- sc = self.managers["strong_coupling"]
+ sc = self.managers["couplings"]
return sc.a_s(
self.sv_exponentiated_shift(self.q2_from), self.q2_from, nf_to=self.nf + 1
)
diff --git a/src/eko/evolution_operator/physical.py b/src/eko/evolution_operator/physical.py
index 7df33af3f..10164fd68 100644
--- a/src/eko/evolution_operator/physical.py
+++ b/src/eko/evolution_operator/physical.py
@@ -1,3 +1,5 @@
+"""Contains the :class:`PhysicalOperator` class."""
+
import numpy as np
from .. import basis_rotation as br
@@ -7,7 +9,7 @@
class PhysicalOperator(member.OperatorBase):
"""
- This joins several fixed flavor scheme operators together.
+ Join several fixed flavor scheme operators together.
- provides the connection between the 7-dimensional anomalous dimension
basis and the 15-dimensional evolution basis
@@ -24,10 +26,9 @@ class PhysicalOperator(member.OperatorBase):
"""
@classmethod
- def ad_to_evol_map(cls, op_members, nf, q2_final, intrinsic_range):
+ def ad_to_evol_map(cls, op_members, nf, q2_final, intrinsic_range, qed=False):
"""
- Obtain map between the 3-dimensional anomalous dimension basis and the
- 4-dimensional evolution basis.
+ Obtain map between the 3-dimensional anomalous dimension basis and the 4-dimensional evolution basis.
.. todo:: in VFNS sometimes IC is irrelevant if nf>=4
@@ -39,6 +40,8 @@ def ad_to_evol_map(cls, op_members, nf, q2_final, intrinsic_range):
number of active light flavors
intrinsic_range : sequence
intrinsic heavy flavors
+ qed : bool
+ activate qed
Returns
-------
@@ -51,19 +54,52 @@ def ad_to_evol_map(cls, op_members, nf, q2_final, intrinsic_range):
"S.g": op_members[(100, 21)],
"g.g": op_members[(21, 21)],
"g.S": op_members[(21, 100)],
- "V.V": op_members[(br.non_singlet_pids_map["nsV"], 0)],
}
- # add elements which are already active
- for f in range(2, nf + 1):
- n = f**2 - 1
- m[f"V{n}.V{n}"] = op_members[(br.non_singlet_pids_map["ns-"], 0)]
- m[f"T{n}.T{n}"] = op_members[(br.non_singlet_pids_map["ns+"], 0)]
+ if not qed:
+ m.update({"V.V": op_members[(br.non_singlet_pids_map["nsV"], 0)]})
+ # add elements which are already active
+ for f in range(2, nf + 1):
+ n = f**2 - 1
+ m[f"V{n}.V{n}"] = op_members[(br.non_singlet_pids_map["ns-"], 0)]
+ m[f"T{n}.T{n}"] = op_members[(br.non_singlet_pids_map["ns+"], 0)]
+ else:
+ m.update(
+ {
+ "g.ph": op_members[(21, 22)],
+ "g.Sdelta": op_members[(21, 101)],
+ "ph.g": op_members[(22, 21)],
+ "ph.ph": op_members[(22, 22)],
+ "ph.S": op_members[(22, 100)],
+ "ph.Sdelta": op_members[(22, 101)],
+ "S.ph": op_members[(100, 22)],
+ "S.Sdelta": op_members[(100, 101)],
+ "Sdelta.g": op_members[(101, 21)],
+ "Sdelta.ph": op_members[(101, 22)],
+ "Sdelta.S": op_members[(101, 100)],
+ "Sdelta.Sdelta": op_members[(101, 101)],
+ "V.V": op_members[(10200, 10200)],
+ "V.Vdelta": op_members[(10200, 10204)],
+ "Vdelta.V": op_members[(10204, 10200)],
+ "Vdelta.Vdelta": op_members[(10204, 10204)],
+ }
+ )
+ # add elements which are already active
+ if nf >= 3:
+ m["Td3.Td3"] = op_members[(br.non_singlet_pids_map["ns+d"], 0)]
+ m["Vd3.Vd3"] = op_members[(br.non_singlet_pids_map["ns-d"], 0)]
+ if nf >= 4:
+ m["Tu3.Tu3"] = op_members[(br.non_singlet_pids_map["ns+u"], 0)]
+ m["Vu3.Vu3"] = op_members[(br.non_singlet_pids_map["ns-u"], 0)]
+ if nf >= 5:
+ m["Td8.Td8"] = op_members[(br.non_singlet_pids_map["ns+d"], 0)]
+ m["Vd8.Vd8"] = op_members[(br.non_singlet_pids_map["ns-d"], 0)]
+ if nf >= 6:
+ m["Tu8.Tu8"] = op_members[(br.non_singlet_pids_map["ns+u"], 0)]
+ m["Vu8.Vu8"] = op_members[(br.non_singlet_pids_map["ns-u"], 0)]
# deal with intrinsic heavy quark PDFs
if intrinsic_range is not None:
hqfl = "cbt"
- op_id = member.OpMember.id_like(
- op_members[(br.non_singlet_pids_map["nsV"], 0)]
- )
+ op_id = member.OpMember.id_like(op_members[(21, 21)])
for intr_fl in intrinsic_range:
if intr_fl <= nf: # light quarks are not intrinsic
continue
@@ -74,25 +110,37 @@ def ad_to_evol_map(cls, op_members, nf, q2_final, intrinsic_range):
# map key to MemberName
return cls.promote_names(m, q2_final)
- def to_flavor_basis_tensor(self):
+ def to_flavor_basis_tensor(self, qed=False):
"""
- Convert the computations into an rank 4 tensor over flavor operator space and
- momentum fraction operator space.
+ Convert the computations into an rank 4 tensor over flavor operator space and momentum fraction operator space.
+
+ Parameters
+ ----------
+ qed : bool
+ activate qed
Returns
-------
tensor : numpy.ndarray
EKO
"""
- nf_in, nf_out = flavors.get_range(self.op_members.keys())
+ nf_in, nf_out = flavors.get_range(self.op_members.keys(), qed)
len_pids = len(br.flavor_basis_pids)
len_xgrid = list(self.op_members.values())[0].value.shape[0]
# dimension will be pids^2 * xgrid^2
value_tensor = np.zeros((len_pids, len_xgrid, len_pids, len_xgrid))
error_tensor = value_tensor.copy()
for name, op in self.op_members.items():
- in_pids = flavors.pids_from_intrinsic_evol(name.input, nf_in, False)
- out_pids = flavors.pids_from_intrinsic_evol(name.target, nf_out, True)
+ if not qed:
+ in_pids = flavors.pids_from_intrinsic_evol(name.input, nf_in, False)
+ out_pids = flavors.pids_from_intrinsic_evol(name.target, nf_out, True)
+ else:
+ in_pids = flavors.pids_from_intrinsic_unified_evol(
+ name.input, nf_in, False
+ )
+ out_pids = flavors.pids_from_intrinsic_unified_evol(
+ name.target, nf_out, True
+ )
for out_idx, out_weight in enumerate(out_pids):
for in_idx, in_weight in enumerate(in_pids):
# keep the outer index to the left as we're multiplying from the right
diff --git a/src/eko/io/manipulate.py b/src/eko/io/manipulate.py
index 8874da5af..15173e800 100644
--- a/src/eko/io/manipulate.py
+++ b/src/eko/io/manipulate.py
@@ -247,3 +247,34 @@ def to_evol(eko: EKO, source: bool = True, target: bool = False):
eko.rotations.targetpids = targetpids
eko.update()
+
+
+def to_uni_evol(eko: EKO, source: bool = True, target: bool = False):
+ """Rotate the operator into evolution basis.
+
+ This also assigns also the pids. The operation is in-place.
+
+ Parameters
+ ----------
+ eko :
+ the operator to be rotated
+ source :
+ rotate on the input tensor
+ target :
+ rotate on the output tensor
+
+ """
+ # rotate
+ inputpids = br.rotate_flavor_to_unified_evolution if source else None
+ targetpids = br.rotate_flavor_to_unified_evolution if target else None
+ # prevent metadata update, since flavor_reshape has not enough information
+ # to determine inpupids and targetpids, and they will be updated after the
+ # call
+ flavor_reshape(eko, inputpids=inputpids, targetpids=targetpids, update=False)
+ # assign pids
+ if source:
+ eko.rotations.inputpids = inputpids
+ if target:
+ eko.rotations.targetpids = targetpids
+
+ eko.update()
diff --git a/src/eko/io/runcards.py b/src/eko/io/runcards.py
index 2ab9a7faa..e0d66944b 100644
--- a/src/eko/io/runcards.py
+++ b/src/eko/io/runcards.py
@@ -280,9 +280,13 @@ def heavies(pattern: str):
new["order"] = [old["PTO"] + 1, old["QED"]]
alphaem = self.fallback(old.get("alphaqed"), old.get("alphaem"), default=0.0)
+ if "QrefQED" not in old:
+ qedref = nan
+ else:
+ qedref = old["QrefQED"]
new["couplings"] = dict(
alphas=(old["alphas"], old["Qref"]),
- alphaem=(alphaem, nan),
+ alphaem=(alphaem, qedref),
num_flavs_ref=old["nfref"],
max_num_flavs=old["MaxNfAs"],
)
diff --git a/src/eko/kernels/as4_evolution_integrals.py b/src/eko/kernels/as4_evolution_integrals.py
index d1e2a9e34..f401de8c6 100644
--- a/src/eko/kernels/as4_evolution_integrals.py
+++ b/src/eko/kernels/as4_evolution_integrals.py
@@ -1,4 +1,4 @@
-"""This file implements the |N3LO| evolution integrals"""
+"""Implement the |N3LO| evolution integrals."""
import numba as nb
import numpy as np
@@ -6,7 +6,9 @@
@nb.njit(cache=True)
def roots(b_list):
- """Returns the roots of:
+ """Return the roots of a third grade polynomial.
+
+ Return the roots of:
.. math ::
1 + b_1 a_s + b_2 a_s^2 + b_3 a_s^3 = 0
@@ -41,7 +43,7 @@ def roots(b_list):
@nb.njit(cache=True)
def derivative(r, b_list):
- r"""Returns the derivative:
+ r"""Return the derivative of a third grade polynomial.
.. math ::
\frac{d}{d a_s}(1 + b_1 a_s + b_2 a_s^2 + b_3 a_s^3) = b_1 + 2 b_2 r + 3 b_3 r^2
@@ -65,8 +67,9 @@ def derivative(r, b_list):
@nb.njit(cache=True)
def j33_exact(a1, a0, beta0, b_list, roots):
- r"""|N3LO|-|N3LO| exact evolution definite integral
- evaluated at :math:`a_s-a_s^0`.
+ r"""|N3LO|-|N3LO| exact evolution definite integral.
+
+ Evaluated at :math:`a_s-a_s^0`.
.. math::
@@ -99,8 +102,9 @@ def j33_exact(a1, a0, beta0, b_list, roots):
@nb.njit(cache=True)
def j23_exact(a1, a0, beta0, b_list, roots):
- r"""|NNLO|-|N3LO| exact evolution definite integral
- evaluated at :math:`a_s-a_s^0`.
+ r"""|NNLO|-|N3LO| exact evolution definite integral.
+
+ Evaluated at :math:`a_s-a_s^0`.
.. math::
@@ -133,8 +137,9 @@ def j23_exact(a1, a0, beta0, b_list, roots):
@nb.njit(cache=True)
def j13_exact(a1, a0, beta0, b_list, roots):
- r"""|NLO|-|N3LO| exact evolution definite integral
- evaluated at :math:`a_s-a_s^0`.
+ r"""|NLO|-|N3LO| exact evolution definite integral.
+
+ Evaluated at :math:`a_s-a_s^0`.
.. math::
@@ -166,9 +171,10 @@ def j13_exact(a1, a0, beta0, b_list, roots):
@nb.njit(cache=True)
-def j03_exact(j00, j13, j23, j33, b_list):
- r"""|LO|-|N3LO| exact evolution definite integral
- evaluated at :math:`a_s-a_s^0`.
+def j03_exact(j12, j13, j23, j33, b_list):
+ r"""|LO|-|N3LO| exact evolution definite integral.
+
+ Evaluated at :math:`a_s-a_s^0`.
.. math::
@@ -176,7 +182,7 @@ def j03_exact(j00, j13, j23, j33, b_list):
Parameters
----------
- j00: float
+ j12: float
|LO|-|LO| evolution integral
j13: float
|NLO|-|N3LO| evolution integral
@@ -194,13 +200,14 @@ def j03_exact(j00, j13, j23, j33, b_list):
"""
b1, b2, b3 = b_list
- return j00 - b1 * j13 - b2 * j23 - b3 * j33
+ return j12 - b1 * j13 - b2 * j23 - b3 * j33
@nb.njit(cache=True)
def j33_expanded(a1, a0, beta0):
- r"""|N3LO|-|N3LO| expanded evolution definite integral
- evaluated at :math:`a_s-a_s^0`.
+ r"""|N3LO|-|N3LO| expanded evolution definite integral.
+
+ Evaluated at :math:`a_s-a_s^0`.
.. math::
j^{(3,3)}_{exp}(a_s) = \frac{1}{3 \beta_0} a_s^3
@@ -225,8 +232,9 @@ def j33_expanded(a1, a0, beta0):
@nb.njit(cache=True)
def j23_expanded(a1, a0, beta0, b_list):
- r"""|NNLO|-|N3LO| expanded evolution definite integral
- evaluated at :math:`a_s-a_s^0`.
+ r"""|NNLO|-|N3LO| expanded evolution definite integral.
+
+ Evaluated at :math:`a_s-a_s^0`.
.. math::
j^{(2,3)}_{exp}(a_s) = \frac{1}{\beta_0} ( \frac{1}{2} a_s^2 - \frac{b_1}{3} as^3)
@@ -254,8 +262,9 @@ def j23_expanded(a1, a0, beta0, b_list):
@nb.njit(cache=True)
def j13_expanded(a1, a0, beta0, b_list):
- r"""|NLO|-|N3LO| expanded evolution definite integral
- evaluated at :math:`a_s-a_s^0`.
+ r"""|NLO|-|N3LO| expanded evolution definite integral.
+
+ Evaluated at :math:`a_s-a_s^0`.
.. math::
j^{(1,3)}_{exp}(a_s) = \frac{1}{\beta_0} ( a_s - \frac{b_1}{2} a_s^2 + \frac{b_1^2-b_2}{3} as^3)
@@ -286,16 +295,17 @@ def j13_expanded(a1, a0, beta0, b_list):
@nb.njit(cache=True)
-def j03_expanded(j00, j13, j23, j33, b_list):
- r"""|LO|-|N3LO| expanded evolution definite integral
- evaluated at :math:`a_s-a_s^0`.
+def j03_expanded(j12, j13, j23, j33, b_list):
+ r"""|LO|-|N3LO| expanded evolution definite integral.
+
+ Evaluated at :math:`a_s-a_s^0`.
.. math::
j^{(0,3)}_{exp}(a_s) = j^{(0,0)} - b_1 j^{(1,3)}_{exp}(a_s) - b_2 j^{(2,3)}_{exp}(a_s) - b_3 j^{(3,3)}_{exp}(a_s)
Parameters
----------
- j00: float
+ j12: float
|LO|-|LO| evolution integral
j13: float
|NLO|-|N3LO| expanded evolution integral
@@ -316,4 +326,4 @@ def j03_expanded(j00, j13, j23, j33, b_list):
j03_exact
"""
- return j03_exact(j00, j13, j23, j33, b_list)
+ return j03_exact(j12, j13, j23, j33, b_list)
diff --git a/src/eko/kernels/evolution_integrals.py b/src/eko/kernels/evolution_integrals.py
index 580c793bb..144a54d64 100644
--- a/src/eko/kernels/evolution_integrals.py
+++ b/src/eko/kernels/evolution_integrals.py
@@ -1,8 +1,9 @@
-r"""
+r"""Compute evolution integrals.
+
Integrals needed for the exact evolutions are given by:
.. math::
- j^{(n,m)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s}\!da_s'\,\frac{(a_s')^{1+n}}{-\beta^{(m)}(a_s')}
+ j^{(n,m)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s}\!da_s'\,\frac{(a_s')^{n}}{-\sum_{i=2}^{m} \beta^{(i)} a_s'^i}
The expanded integrals are obtained from the exact results by Taylor expanding in the limit
:math:`a_s,a_s^{0} \to 0` until :math:`\mathcal{O}( a_s^{m+1})` for :math:`N^{m}LO` computations.
@@ -11,16 +12,14 @@
import numba as nb
import numpy as np
-from .. import beta
-
@nb.njit(cache=True)
-def j00(a1, a0, nf):
+def j12(a1, a0, beta0):
r"""
- LO-LO exact evolution integral.
+ :math:`j^{(1,2)}` exact evolution integral.
.. math::
- j^{(0,0)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s} \frac{da_s'}{\beta_0 a_s'}
+ j^{(1,2)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s} \frac{da_s'}{\beta_0 a_s'}
= \frac{\ln(a_s/a_s^0)}{\beta_0}
Parameters
@@ -29,21 +28,21 @@ def j00(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
Returns
-------
- j00 : float
+ j12 : float
integral
"""
- return np.log(a1 / a0) / beta.beta_qcd((2, 0), nf)
+ return np.log(a1 / a0) / beta0
@nb.njit(cache=True)
-def j11_exact(a1, a0, nf):
+def j23_exact(a1, a0, beta0, b_vec):
r"""
- NLO-NLO exact evolution integral.
+ :math:`j^{(2,3)}` exact evolution integral.
.. math::
j^{(1,1)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s}\!da_s'\,
@@ -56,26 +55,27 @@ def j11_exact(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
Returns
-------
- j11 : float
+ j23_exact : float
integral
"""
- beta_qcd_as3 = beta.beta_qcd((3, 0), nf)
- b1 = beta.b_qcd((3, 0), nf)
- return (1.0 / beta_qcd_as3) * np.log((1.0 + a1 * b1) / (1.0 + a0 * b1))
+ b1 = b_vec[1]
+ return (1.0 / (b1 * beta0)) * np.log((1.0 + a1 * b1) / (1.0 + a0 * b1))
@nb.njit(cache=True)
-def j11_expanded(a1, a0, nf):
+def j23_expanded(a1, a0, beta0):
r"""
- NLO-NLO expanded evolution integral.
+ :math:`j^{(2,3)}` expanded evolution integral.
.. math::
- j^{(1,1)}_{exp}(a_s,a_s^0) = \frac 1 {\beta_0}(a_s - a_s^0)
+ j^{(2,3)}_{exp}(a_s,a_s^0) = \frac 1 {\beta_0}(a_s - a_s^0)
Parameters
----------
@@ -83,26 +83,26 @@ def j11_expanded(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
Returns
-------
- j11_exp : float
+ j23_expanded : float
integral
"""
- return 1.0 / beta.beta_qcd((2, 0), nf) * (a1 - a0)
+ return 1.0 / beta0 * (a1 - a0)
@nb.njit(cache=True)
-def j01_exact(a1, a0, nf):
+def j13_exact(a1, a0, beta0, b_vec):
r"""
- LO-NLO exact evolution integral.
+ :math:`j^{(1,3)}` exact evolution integral.
.. math::
- j^{(0,1)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s}\!da_s'\,
+ j^{(1,3)}(a_s,a_s^0) = \int\limits_{a_s^0}^{a_s}\!da_s'\,
\frac{a_s'}{\beta_0 a_s'^2 + \beta_1 a_s'^3}
- = j^{(0,0)}(a_s,a_s^0) - b_1 j^{(1,1)}(a_s,a_s^0)
+ = j^{(0,0)}(a_s,a_s^0) - b_1 j^{(2,3)}(a_s,a_s^0)
Parameters
----------
@@ -110,24 +110,27 @@ def j01_exact(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
Returns
-------
- j11 : float
+ j13_exact : float
integral
"""
- return j00(a1, a0, nf) - beta.b_qcd((3, 0), nf) * j11_exact(a1, a0, nf)
+ b1 = b_vec[1]
+ return j12(a1, a0, beta0) - b1 * j23_exact(a1, a0, beta0, b_vec)
@nb.njit(cache=True)
-def j01_expanded(a1, a0, nf):
+def j13_expanded(a1, a0, beta0, b_vec):
r"""
- LO-NLO expanded evolution integral.
+ :math:`j^{(1,3)}` expanded evolution integral.
.. math::
- j^{(0,1)}_{exp}(a_s,a_s^0) = j^{(0,0)}(a_s,a_s^0) - b_1 j^{(1,1)}_{exp}(a_s,a_s^0)
+ j^{(1,3)}_{exp}(a_s,a_s^0) = j^{(0,0)}(a_s,a_s^0) - b_1 j^{(2,3)}_{exp}(a_s,a_s^0)
Parameters
----------
@@ -135,24 +138,27 @@ def j01_expanded(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
Returns
-------
- j01_exp : float
+ j13_expanded : float
integral
"""
- return j00(a1, a0, nf) - beta.b_qcd((3, 0), nf) * j11_expanded(a1, a0, nf)
+ b1 = b_vec[1]
+ return j12(a1, a0, beta0) - b1 * j23_expanded(a1, a0, beta0)
@nb.njit(cache=True)
-def j22_exact(a1, a0, nf):
+def j34_exact(a1, a0, beta0, b_vec):
r"""
- NNLO-NNLO exact evolution integral.
+ :math:`j^{(3,4)}` exact evolution integral.
.. math::
- j^{(2,2)}(a_s,a_s^0) &=
+ j^{(3,4)}(a_s,a_s^0) &=
\int\limits_{a_s^0}^{a_s}\!da_s'\,\frac{a_s'^3}
{\beta_0 a_s'^2 + \beta_1 a_s'^3 + \beta_2 a_s'^4}
= \frac{1}{\beta_2}\ln\left(
@@ -168,31 +174,34 @@ def j22_exact(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
+ beta2 : float
+ NNLO beta function coefficient
Returns
-------
- j22 : complex
+ j34_exact : complex
integral
"""
- b1 = beta.b_qcd((3, 0), nf)
- b2 = beta.b_qcd((4, 0), nf)
+ b1 = b_vec[1]
+ b2 = b_vec[2]
+ beta2 = b2 * beta0
# allow Delta to be complex for nf = 6, the final result will be real
Delta = np.sqrt(complex(4 * b2 - b1**2))
delta = np.arctan((b1 + 2 * a1 * b2) / Delta) - np.arctan(
(b1 + 2 * a0 * b2) / Delta
)
log = np.log((1 + a1 * (b1 + b2 * a1)) / (1 + a0 * (b1 + b2 * a0)))
- return 1 / (2 * beta.beta_qcd((4, 0), nf)) * log - b1 / (
- beta.beta_qcd((4, 0), nf)
- ) * np.real(delta / Delta)
+ return 1 / (2 * beta2) * log - b1 / (beta2) * np.real(delta / Delta)
@nb.njit(cache=True)
-def j12_exact(a1, a0, nf):
+def j24_exact(a1, a0, beta0, b_vec):
r"""
- NLO-NNLO exact evolution integral.
+ :math:`j^{(2,4)}` exact evolution integral.
.. math::
j^{(1,2)}(a_s,a_s^0) &= \int\limits_{a_s^0}^{a_s}\!da_s'\,\frac{a_s'^2}{\beta_0 a_s'^2 + \beta_1 a_s'^3 + \beta_2 a_s'^4}\\
@@ -206,33 +215,37 @@ def j12_exact(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
+ beta2 : float
+ NNLO beta function coefficient
Returns
-------
- j12 : complex
+ j24_exact : complex
integral
""" # pylint: disable=line-too-long
- b1 = beta.b_qcd((3, 0), nf)
- b2 = beta.b_qcd((4, 0), nf)
+ b1 = b_vec[1]
+ b2 = b_vec[2]
# allow Delta to be complex for nf = 6, the final result will be real
Delta = np.sqrt(complex(4 * b2 - b1**2))
delta = np.arctan((b1 + 2 * a1 * b2) / Delta) - np.arctan(
(b1 + 2 * a0 * b2) / Delta
)
- return 2.0 / (beta.beta_qcd((2, 0), nf)) * np.real(delta / Delta)
+ return 2.0 / (beta0) * np.real(delta / Delta)
@nb.njit(cache=True)
-def j02_exact(a1, a0, nf):
+def j14_exact(a1, a0, beta0, b_vec):
r"""
- LO-NNLO exact evolution integral.
+ :math:`j^{(1,4)}` exact evolution integral.
.. math::
j^{(0,2)}(a_s,a_s^0) &= \int\limits_{a_s^0}^{a_s}\!da_s'\,
\frac{a_s'}{\beta_0 a_s'^2 + \beta_1 a_s'^3 + \beta_2 a_s'^4}\\
- &= j^{(0,0)}(a_s,a_s^0) - b_1 j^{(1,2)}(a_s,a_s^0) - b_2 j^{(2,2)}(a_s,a_s^0)
+ &= j^{(0,0)}(a_s,a_s^0) - b_1 j^{(2,4)}(a_s,a_s^0) - b_2 j^{(3,4)}(a_s,a_s^0)
Parameters
----------
@@ -240,25 +253,31 @@ def j02_exact(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
+ beta2 : float
+ NNLO beta function coefficient
Returns
-------
- j02 : complex
+ j14_exact : complex
integral
"""
+ b1 = b_vec[1]
+ b2 = b_vec[2]
return (
- j00(a1, a0, nf)
- - beta.b_qcd((3, 0), nf) * j12_exact(a1, a0, nf)
- - beta.b_qcd((4, 0), nf) * j22_exact(a1, a0, nf)
+ j12(a1, a0, beta0)
+ - b1 * j24_exact(a1, a0, beta0, b_vec)
+ - b2 * j34_exact(a1, a0, beta0, b_vec)
)
@nb.njit(cache=True)
-def j22_expanded(a1, a0, nf):
+def j34_expanded(a1, a0, beta0):
r"""
- NNLO-NNLO expanded evolution integral.
+ :math:`j^{(3,4)}` expanded evolution integral.
.. math::
j^{(2,2)}_{exp}(a_s,a_s^0) = \frac{1}{2 \beta_0} \left( a_s^2 - (a_s^0)^{2} \right)
@@ -269,24 +288,24 @@ def j22_expanded(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
Returns
-------
- j22_exp : float
+ j34_expanded : float
integral
"""
- return 1 / (2 * beta.beta_qcd((2, 0), nf)) * (a1**2 - a0**2)
+ return 1 / (2 * beta0) * (a1**2 - a0**2)
@nb.njit(cache=True)
-def j12_expanded(a1, a0, nf):
+def j24_expanded(a1, a0, beta0, b_vec):
r"""
- NLO-NNLO expanded evolution integral.
+ :math:`j^{(2,4)}` expanded evolution integral.
.. math::
- j^{(1,2)}_{exp}(a_s,a_s^0) = \frac{1}{\beta_0}\left[ a_s - a_s^0 -
+ j^{(2,4)}_{exp}(a_s,a_s^0) = \frac{1}{\beta_0}\left[ a_s - a_s^0 -
\frac{b_1}{2} \left( a_s^2 - (a_s^0)^{2} \right)\right]
Parameters
@@ -295,26 +314,28 @@ def j12_expanded(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
Returns
-------
- j12_exp : float
+ j24_expanded : float
integral
"""
- b1 = beta.b_qcd((3, 0), nf)
- return 1 / beta.beta_qcd((2, 0), nf) * (a1 - a0 - b1 / 2 * (a1**2 - a0**2))
+ b1 = b_vec[1]
+ return 1 / beta0 * (a1 - a0 - b1 / 2 * (a1**2 - a0**2))
@nb.njit(cache=True)
-def j02_expanded(a1, a0, nf):
+def j14_expanded(a1, a0, beta0, b_vec):
r"""
- LO-NNLO expanded evolution integral.
+ :math:`j^{(1,4)}` expanded evolution integral.
.. math::
- j^{(0,2)}_{exp}(a_s,a_s^0) = j^{(0,0)}(a_s,a_s^0) - b_1 j^{(1,2)}_{exp}(a_s,a_s^0)
- - b_2 j^{(2,2)}_{exp}(a_s,a_s^0)
+ j^{(1,4)}_{exp}(a_s,a_s^0) = j^{(0,0)}(a_s,a_s^0) - b_1 j^{(2,4)}_{exp}(a_s,a_s^0)
+ - b_2 j^{(3,4)}_{exp}(a_s,a_s^0)
Parameters
----------
@@ -322,16 +343,22 @@ def j02_expanded(a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta0 : float
+ LO beta function coefficient
+ beta1 : float
+ NLO beta function coefficient
+ beta2 : float
+ NNLO beta function coefficient
Returns
-------
- j02_exp : float
+ j14_expanded : float
integral
"""
+ b1 = b_vec[1]
+ b2 = b_vec[2]
return (
- j00(a1, a0, nf)
- - beta.b_qcd((3, 0), nf) * j12_expanded(a1, a0, nf)
- - beta.b_qcd((4, 0), nf) * j22_expanded(a1, a0, nf)
+ j12(a1, a0, beta0)
+ - b1 * j24_expanded(a1, a0, beta0, b_vec)
+ - b2 * j34_expanded(a1, a0, beta0)
)
diff --git a/src/eko/kernels/non_singlet.py b/src/eko/kernels/non_singlet.py
index ab9838572..24706a794 100644
--- a/src/eko/kernels/non_singlet.py
+++ b/src/eko/kernels/non_singlet.py
@@ -1,6 +1,4 @@
-"""
-Collection of non-singlet EKOs.
-"""
+"""Collection of non-singlet EKOs."""
import numba as nb
import numpy as np
@@ -12,15 +10,15 @@
@nb.njit(cache=True)
-def U_vec(gamma_ns, nf, order):
+def U_vec(gamma_ns, beta, order):
r"""Compute the elements of the non-singlet U vector.
Parameters
----------
gamma_ns : numpy.ndarray
non-singlet anomalous dimension
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
order : int
perturbative order
@@ -31,160 +29,165 @@ def U_vec(gamma_ns, nf, order):
"""
U = np.zeros(order[0], dtype=np.complex_)
- beta0 = beta.beta_qcd((2, 0), nf)
+ beta0 = beta[0]
R0 = gamma_ns[0] / beta0
U[0] = 1.0
if order[0] >= 2:
- b1 = beta.b_qcd((3, 0), nf)
+ b1 = beta[1] / beta[0]
R1 = gamma_ns[1] / beta0 - b1 * R0
U[1] = R1
if order[0] >= 3:
- b2 = beta.b_qcd((4, 0), nf)
+ b2 = beta[2] / beta[0]
R2 = gamma_ns[2] / beta0 - b1 * R1 - b2 * R0
U[2] = 0.5 * (R2 + U[1] * R1)
if order[0] >= 4:
- b3 = beta.b_qcd((5, 0), nf)
+ b3 = beta[3] / beta[0]
R3 = gamma_ns[3] / beta0 - b1 * R2 - b2 * R1 - b3 * R0
U[3] = 1 / 3 * (R3 + R2 * U[1] + R1 * U[2])
return U
@nb.njit(cache=True)
-def lo_exact(gamma_ns, a1, a0, nf):
- """
- |LO| non-singlet exact EKO
+def lo_exact(gamma_ns, a1, a0, beta):
+ """|LO| non-singlet exact EKO.
Parameters
----------
- gamma_ns : numpy.ndarray
- non-singlet anomalous dimensions
- a1 : float
- target coupling value
- a0 : float
- initial coupling value
- nf : int
- number of active flavors
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta : list
+ list of the values of the beta functions
Returns
-------
- e_ns^0 : complex
- |LO| non-singlet exact EKO
+ e_ns^0 : complex
+ |LO| non-singlet exact EKO
"""
- return np.exp(gamma_ns[0] * ei.j00(a1, a0, nf))
+ beta0 = beta[0]
+ return np.exp(gamma_ns[0] * ei.j12(a1, a0, beta0))
@nb.njit(cache=True)
-def nlo_exact(gamma_ns, a1, a0, nf):
- """
- |NLO| non-singlet exact EKO
+def nlo_exact(gamma_ns, a1, a0, beta):
+ """|NLO| non-singlet exact EKO.
Parameters
----------
- gamma_ns : numpy.ndarray
- non-singlet anomalous dimensions
- a1 : float
- target coupling value
- a0 : float
- initial coupling value
- nf : int
- number of active flavors
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta : list
+ list of the values of the beta functions
Returns
-------
- e_ns^1 : complex
- |NLO| non-singlet exact EKO
+ e_ns^1 : complex
+ |NLO| non-singlet exact EKO
"""
+ beta0 = beta[0]
+ b_vec = [betas / beta0 for betas in beta]
return np.exp(
- gamma_ns[0] * ei.j01_exact(a1, a0, nf) + gamma_ns[1] * ei.j11_exact(a1, a0, nf)
+ gamma_ns[0] * ei.j13_exact(a1, a0, beta0, b_vec)
+ + gamma_ns[1] * ei.j23_exact(a1, a0, beta0, b_vec)
)
@nb.njit(cache=True)
-def nlo_expanded(gamma_ns, a1, a0, nf):
- """
- |NLO| non-singlet expanded EKO
+def nlo_expanded(gamma_ns, a1, a0, beta):
+ """|NLO| non-singlet expanded EKO.
Parameters
----------
- gamma_ns : numpy.ndarray
- non-singlet anomalous dimensions
- a1 : float
- target coupling value
- a0 : float
- initial coupling value
- nf : int
- number of active flavors
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta : list
+ list of the values of the beta functions
Returns
-------
- e_ns^1 : complex
- |NLO| non-singlet expanded EKO
+ e_ns^1 : complex
+ |NLO| non-singlet expanded EKO
"""
+ beta0 = beta[0]
+ b_vec = [betas / beta0 for betas in beta]
return np.exp(
- gamma_ns[0] * ei.j01_expanded(a1, a0, nf)
- + gamma_ns[1] * ei.j11_expanded(a1, a0, nf)
+ gamma_ns[0] * ei.j13_expanded(a1, a0, beta0, b_vec)
+ + gamma_ns[1] * ei.j23_expanded(a1, a0, beta0)
)
@nb.njit(cache=True)
-def nnlo_exact(gamma_ns, a1, a0, nf):
- """
- |NNLO| non-singlet exact EKO
+def nnlo_exact(gamma_ns, a1, a0, beta):
+ """|NNLO| non-singlet exact EKO.
Parameters
----------
- gamma_ns : numpy.ndarray
- non-singlet anomalous dimensions
- a1 : float
- target coupling value
- a0 : float
- initial coupling value
- nf : int
- number of active flavors
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta : list
+ list of the values of the beta functions
Returns
-------
- e_ns^2 : complex
- |NNLO| non-singlet exact EKO
+ e_ns^2 : complex
+ |NNLO| non-singlet exact EKO
"""
+ beta0 = beta[0]
+ b_vec = [betas / beta0 for betas in beta]
return np.exp(
- gamma_ns[0] * ei.j02_exact(a1, a0, nf)
- + gamma_ns[1] * ei.j12_exact(a1, a0, nf)
- + gamma_ns[2] * ei.j22_exact(a1, a0, nf)
+ gamma_ns[0] * ei.j14_exact(a1, a0, beta0, b_vec)
+ + gamma_ns[1] * ei.j24_exact(a1, a0, beta0, b_vec)
+ + gamma_ns[2] * ei.j34_exact(a1, a0, beta0, b_vec)
)
@nb.njit(cache=True)
-def nnlo_expanded(gamma_ns, a1, a0, nf):
- """
- |NNLO| non-singlet expanded EKO
+def nnlo_expanded(gamma_ns, a1, a0, beta):
+ """|NNLO| non-singlet expanded EKO.
Parameters
----------
- gamma_ns : numpy.ndarray
- non-singlet anomalous dimensions
- a1 : float
- target coupling value
- a0 : float
- initial coupling value
- nf : int
- number of active flavors
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta : list
+ list of the values of the beta functions
Returns
-------
- e_ns^2 : complex
- |NNLO| non-singlet expanded EKO
+ e_ns^2 : complex
+ |NNLO| non-singlet expanded EKO
"""
+ beta0 = beta[0]
+ b_vec = [betas / beta0 for betas in beta]
return np.exp(
- gamma_ns[0] * ei.j02_expanded(a1, a0, nf)
- + gamma_ns[1] * ei.j12_expanded(a1, a0, nf)
- + gamma_ns[2] * ei.j22_expanded(a1, a0, nf)
+ gamma_ns[0] * ei.j14_expanded(a1, a0, beta0, b_vec)
+ + gamma_ns[1] * ei.j24_expanded(a1, a0, beta0, b_vec)
+ + gamma_ns[2] * ei.j34_expanded(a1, a0, beta0)
)
@nb.njit(cache=True)
def n3lo_expanded(gamma_ns, a1, a0, nf):
- """|N3LO| non-singlet expanded EKO
+ """|N3LO| non-singlet expanded EKO.
Parameters
----------
@@ -209,12 +212,12 @@ def n3lo_expanded(gamma_ns, a1, a0, nf):
beta.b_qcd((4, 0), nf),
beta.b_qcd((5, 0), nf),
]
- j00 = ei.j00(a1, a0, nf)
+ j12 = ei.j12(a1, a0, beta0)
j13 = as4_ei.j13_expanded(a1, a0, beta0, b_list)
j23 = as4_ei.j23_expanded(a1, a0, beta0, b_list)
j33 = as4_ei.j33_expanded(a1, a0, beta0)
return np.exp(
- gamma_ns[0] * as4_ei.j03_expanded(j00, j13, j23, j33, b_list)
+ gamma_ns[0] * as4_ei.j03_expanded(j12, j13, j23, j33, b_list)
+ gamma_ns[1] * j13
+ gamma_ns[2] * j23
+ gamma_ns[3] * j33
@@ -223,7 +226,7 @@ def n3lo_expanded(gamma_ns, a1, a0, nf):
@nb.njit(cache=True)
def n3lo_exact(gamma_ns, a1, a0, nf):
- """|N3LO| non-singlet exact EKO
+ """|N3LO| non-singlet exact EKO.
Parameters
----------
@@ -249,12 +252,12 @@ def n3lo_exact(gamma_ns, a1, a0, nf):
beta.b_qcd((5, 0), nf),
]
roots = as4_ei.roots(b_list)
- j00 = ei.j00(a1, a0, nf)
+ j12 = ei.j12(a1, a0, beta0)
j13 = as4_ei.j13_exact(a1, a0, beta0, b_list, roots)
j23 = as4_ei.j23_exact(a1, a0, beta0, b_list, roots)
j33 = as4_ei.j33_exact(a1, a0, beta0, b_list, roots)
return np.exp(
- gamma_ns[0] * as4_ei.j03_exact(j00, j13, j23, j33, b_list)
+ gamma_ns[0] * as4_ei.j03_exact(j12, j13, j23, j33, b_list)
+ gamma_ns[1] * j13
+ gamma_ns[2] * j23
+ gamma_ns[3] * j33
@@ -262,8 +265,8 @@ def n3lo_exact(gamma_ns, a1, a0, nf):
@nb.njit(cache=True)
-def eko_ordered_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations):
- """|NLO|, |NNLO| or |N3LO| non-singlet ordered truncated EKO
+def eko_ordered_truncated(gamma_ns, a1, a0, beta, order, ev_op_iterations):
+ """|NLO|, |NNLO| or |N3LO| non-singlet ordered truncated EKO.
Parameters
----------
@@ -273,8 +276,8 @@ def eko_ordered_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
order : tuple(int,int)
perturbative order
ev_op_iterations : int
@@ -287,11 +290,11 @@ def eko_ordered_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations):
"""
a_steps = utils.geomspace(a0, a1, 1 + ev_op_iterations)
- U = U_vec(gamma_ns, nf, order)
+ U = U_vec(gamma_ns, beta, order)
e = 1.0
al = a_steps[0]
for ah in a_steps[1:]:
- e0 = lo_exact(gamma_ns, ah, al, nf)
+ e0 = lo_exact(gamma_ns, ah, al, beta)
num, den = 0, 0
for i in range(order[0]):
num += U[i] * ah**i
@@ -302,8 +305,8 @@ def eko_ordered_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations):
@nb.njit(cache=True)
-def eko_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations):
- """|NLO|, |NNLO| or |N3LO| non-singlet truncated EKO
+def eko_truncated(gamma_ns, a1, a0, beta, order, ev_op_iterations):
+ """|NLO|, |NNLO| or |N3LO| non-singlet truncated EKO.
Parameters
----------
@@ -313,8 +316,8 @@ def eko_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
order : tuple(int,int)
perturbative order
ev_op_iterations : int
@@ -327,12 +330,12 @@ def eko_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations):
"""
a_steps = utils.geomspace(a0, a1, 1 + ev_op_iterations)
- U = U_vec(gamma_ns, nf, order)
+ U = U_vec(gamma_ns, beta, order)
e = 1.0
al = a_steps[0]
fact = U[0]
for ah in a_steps[1:]:
- e0 = lo_exact(gamma_ns, ah, al, nf)
+ e0 = lo_exact(gamma_ns, ah, al, beta)
if order[0] >= 2:
fact += U[1] * (ah - al)
if order[0] >= 3:
@@ -380,13 +383,16 @@ def dispatcher(
non-singlet EKO
"""
+ betalist = [beta.beta_qcd((2 + i, 0), nf) for i in range(order[0])]
# use always exact in LO
if order[0] == 1:
- return lo_exact(gamma_ns, a1, a0, nf)
+ return lo_exact(gamma_ns, a1, a0, betalist)
if method == "ordered-truncated":
- return eko_ordered_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations)
+ return eko_ordered_truncated(
+ gamma_ns, a1, a0, betalist, order, ev_op_iterations
+ )
if method == "truncated":
- return eko_truncated(gamma_ns, a1, a0, nf, order, ev_op_iterations)
+ return eko_truncated(gamma_ns, a1, a0, betalist, order, ev_op_iterations)
# NLO
if order[0] == 2:
@@ -395,9 +401,9 @@ def dispatcher(
"decompose-expanded",
"perturbative-expanded",
]:
- return nlo_expanded(gamma_ns, a1, a0, nf)
+ return nlo_expanded(gamma_ns, a1, a0, betalist)
# if method in ["iterate-exact", "decompose-exact", "perturbative-exact"]:
- return nlo_exact(gamma_ns, a1, a0, nf)
+ return nlo_exact(gamma_ns, a1, a0, betalist)
# NNLO
if order[0] == 3:
if method in [
@@ -405,8 +411,8 @@ def dispatcher(
"decompose-expanded",
"perturbative-expanded",
]:
- return nnlo_expanded(gamma_ns, a1, a0, nf)
- return nnlo_exact(gamma_ns, a1, a0, nf)
+ return nnlo_expanded(gamma_ns, a1, a0, betalist)
+ return nnlo_exact(gamma_ns, a1, a0, betalist)
# N3LO
if order[0] == 4:
if method in [
diff --git a/src/eko/kernels/non_singlet_qed.py b/src/eko/kernels/non_singlet_qed.py
new file mode 100644
index 000000000..a770687e4
--- /dev/null
+++ b/src/eko/kernels/non_singlet_qed.py
@@ -0,0 +1,263 @@
+"""Collection of QED non-singlet EKOs."""
+import numba as nb
+import numpy as np
+
+from .. import beta
+from . import non_singlet as ns
+from . import utils
+
+
+@nb.njit(cache=True)
+def contract_gammas(gamma_ns, aem):
+ """Contract anomalous dimension along the QED axis.
+
+ Parameters
+ ----------
+ gamma_ns : 2D numpy.ndarray
+ non-singlet anomalous dimensions
+ aem : float
+ electromagnetic coupling value
+
+ Returns
+ -------
+ gamma_ns : 1D numpy.ndarray
+ non-singlet anomalous dimensions
+ """
+ vec_alphaem = np.array(
+ [aem**i for i in range(gamma_ns.shape[1])], dtype=np.complex_
+ )
+ return gamma_ns @ vec_alphaem
+
+
+@nb.njit(cache=True)
+def apply_qed(gamma_pure_qed, mu2_from, mu2_to):
+ """Apply pure QED evolution to QCD kernel.
+
+ Parameters
+ ----------
+ gamma_ns : float
+ pure QED part of the AD
+ mu2_from : float
+ initial value of mu2
+ mu2_from : float
+ final value of mu2
+
+ Returns
+ -------
+ exp : float
+ pure QED evolution kernel
+
+ """
+ return np.exp(gamma_pure_qed * np.log(mu2_from / mu2_to))
+
+
+@nb.njit(cache=True)
+def as1_exact(gamma_ns, a1, a0, beta):
+ """O(as1aem1) non-singlet exact EKO.
+
+ Parameters
+ ----------
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta : list
+ list of the values of the beta functions
+
+ Returns
+ -------
+ e_ns^0 : complex
+ O(as1aem1) non-singlet exact EKO
+ """
+ return ns.lo_exact(gamma_ns, a1, a0, beta)
+
+
+@nb.njit(cache=True)
+def as2_exact(gamma_ns, a1, a0, beta):
+ """O(as2aem1) non-singlet exact EKO.
+
+ Parameters
+ ----------
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta : list
+ list of the values of the beta functions
+
+ Returns
+ -------
+ e_ns^1 : complex
+ O(as2aem1) non-singlet exact EKO
+ """
+ return ns.nlo_exact(gamma_ns, a1, a0, beta)
+
+
+@nb.njit(cache=True)
+def as3_exact(gamma_ns, a1, a0, beta):
+ """O(as3aem1) non-singlet exact EKO.
+
+ Parameters
+ ----------
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ beta : list
+ list of the values of the beta functions
+
+ Returns
+ -------
+ e_ns^2 : complex
+ O(as3aem1) non-singlet exact EKO
+ """
+ return ns.nnlo_exact(gamma_ns, a1, a0, beta)
+
+
+@nb.njit(cache=True)
+def dispatcher(
+ order,
+ method,
+ gamma_ns,
+ as_list,
+ aem_half,
+ alphaem_running,
+ nf,
+ ev_op_iterations,
+ mu2_to,
+ mu2_from,
+):
+ r"""Determine used kernel and call it.
+
+ In LO we always use the exact solution.
+
+ Parameters
+ ----------
+ order : tuple(int,int)
+ perturbation order
+ method : str
+ method
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ aem_list : numpy.ndarray
+ electromagnetic coupling values
+ alphaem_running : Bool
+ running of alphaem
+ nf : int
+ number of active flavors
+ ev_op_iterations : int
+ number of evolution steps
+ mu2_from : float
+ initial value of mu2
+ mu2_from : float
+ final value of mu2
+
+ Returns
+ -------
+ e_ns : complex
+ non-singlet EKO
+ """
+ return exact(
+ order,
+ gamma_ns,
+ as_list,
+ aem_half,
+ nf,
+ ev_op_iterations,
+ mu2_to,
+ mu2_from,
+ )
+
+
+@nb.njit(cache=True)
+def fixed_alphaem_exact(order, gamma_ns, a1, a0, aem, nf, mu2_to, mu2_from):
+ """Compute exact solution for fixed alphaem.
+
+ Parameters
+ ----------
+ order : tuple(int,int)
+ perturbation order
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ aem : float
+ electromagnetic coupling value
+ nf : int
+ number of active flavors
+ mu2_from : float
+ initial value of mu2
+ mu2_from : float
+ final value of mu2
+
+ Returns
+ -------
+ e_ns : complex
+ non-singlet EKO
+ """
+ betalist = [beta.beta_qcd((2 + i, 0), nf) for i in range(order[0])]
+ betalist[0] += aem * beta.beta_qcd((2, 1), nf)
+ gamma_ns_list = contract_gammas(gamma_ns, aem)
+ if order[0] == 1:
+ qcd_only = as1_exact(gamma_ns_list[1:], a1, a0, betalist)
+ elif order[0] == 2:
+ qcd_only = as2_exact(gamma_ns_list[1:], a1, a0, betalist)
+ elif order[0] == 3:
+ qcd_only = as3_exact(gamma_ns_list[1:], a1, a0, betalist)
+ else:
+ raise NotImplementedError("Selected order is not implemented")
+ return qcd_only * apply_qed(gamma_ns_list[0], mu2_from, mu2_to)
+
+
+@nb.njit(cache=True)
+def exact(order, gamma_ns, as_list, aem_half, nf, ev_op_iterations, mu2_to, mu2_from):
+ """Compute exact solution for running alphaem.
+
+ Parameters
+ ----------
+ order : tuple(int,int)
+ perturbation order
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ aem_list : numpy.ndarray
+ electromagnetic coupling values
+ nf : int
+ number of active flavors
+ ev_op_iterations : int
+ number of evolution steps
+ mu2_from : float
+ initial value of mu2
+ mu2_from : float
+ final value of mu2
+
+ Returns
+ -------
+ e_ns : complex
+ non-singlet EKO
+ """
+ mu2_steps = utils.geomspace(mu2_from, mu2_to, 1 + ev_op_iterations)
+ res = 1.0
+ for step in range(1, ev_op_iterations + 1):
+ aem = aem_half[step - 1]
+ a1 = as_list[step]
+ a0 = as_list[step - 1]
+ mu2_from = mu2_steps[step - 1]
+ mu2_to = mu2_steps[step]
+ res *= fixed_alphaem_exact(order, gamma_ns, a1, a0, aem, nf, mu2_to, mu2_from)
+ return res
diff --git a/src/eko/kernels/singlet.py b/src/eko/kernels/singlet.py
index 2bab5d35c..159135dc6 100644
--- a/src/eko/kernels/singlet.py
+++ b/src/eko/kernels/singlet.py
@@ -4,6 +4,7 @@
import numpy as np
from ekore import anomalous_dimensions as ad
+
from .. import beta
from . import as4_evolution_integrals as as4_ei
from . import evolution_integrals as ei
@@ -11,7 +12,7 @@
@nb.njit(cache=True)
-def lo_exact(gamma_singlet, a1, a0, nf):
+def lo_exact(gamma_singlet, a1, a0, beta):
"""Singlet leading order exact EKO.
Parameters
@@ -22,15 +23,15 @@ def lo_exact(gamma_singlet, a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
Returns
-------
numpy.ndarray
singlet leading order exact EKO
"""
- return ad.exp_singlet(gamma_singlet[0] * ei.j00(a1, a0, nf))[0]
+ return ad.exp_matrix_2D(gamma_singlet[0] * ei.j12(a1, a0, beta[0]))[0]
@nb.njit(cache=True)
@@ -41,12 +42,6 @@ def nlo_decompose(gamma_singlet, j01, j11):
----------
gamma_singlet : numpy.ndarray
singlet anomalous dimensions matrices
- a1 : float
- target coupling value
- a0 : float
- initial coupling value
- nf : int
- number of active flavors
j01 : float
|LO|-|NLO| evolution integral
j11 : float
@@ -57,11 +52,11 @@ def nlo_decompose(gamma_singlet, j01, j11):
numpy.ndarray
singlet next-to-leading order decompose EKO
"""
- return ad.exp_singlet(gamma_singlet[0] * j01 + gamma_singlet[1] * j11)[0]
+ return ad.exp_matrix_2D(gamma_singlet[0] * j01 + gamma_singlet[1] * j11)[0]
@nb.njit(cache=True)
-def nlo_decompose_exact(gamma_singlet, a1, a0, nf):
+def nlo_decompose_exact(gamma_singlet, a1, a0, beta):
"""Singlet next-to-leading order decompose-exact EKO.
Parameters
@@ -72,21 +67,25 @@ def nlo_decompose_exact(gamma_singlet, a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
Returns
-------
numpy.ndarray
singlet next-to-leading order decompose-exact EKO
"""
+ beta0 = beta[0]
+ b_vec = [betas / beta0 for betas in beta]
return nlo_decompose(
- gamma_singlet, ei.j01_exact(a1, a0, nf), ei.j11_exact(a1, a0, nf)
+ gamma_singlet,
+ ei.j13_exact(a1, a0, beta0, b_vec),
+ ei.j23_exact(a1, a0, beta0, b_vec),
)
@nb.njit(cache=True)
-def nlo_decompose_expanded(gamma_singlet, a1, a0, nf):
+def nlo_decompose_expanded(gamma_singlet, a1, a0, beta):
"""Singlet next-to-leading order decompose-expanded EKO.
Parameters
@@ -97,16 +96,20 @@ def nlo_decompose_expanded(gamma_singlet, a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
Returns
-------
numpy.ndarray
singlet next-to-leading order decompose-expanded EKO
"""
+ beta0 = beta[0]
+ b_vec = [betas / beta0 for betas in beta]
return nlo_decompose(
- gamma_singlet, ei.j01_expanded(a1, a0, nf), ei.j11_expanded(a1, a0, nf)
+ gamma_singlet,
+ ei.j13_expanded(a1, a0, beta0, b_vec),
+ ei.j23_expanded(a1, a0, beta0),
)
@@ -118,12 +121,6 @@ def nnlo_decompose(gamma_singlet, j02, j12, j22):
----------
gamma_singlet : numpy.ndarray
singlet anomalous dimensions matrices
- a1 : float
- target coupling value
- a0 : float
- initial coupling value
- nf : int
- number of active flavors
j02 : float
LO-NNLO evolution integral
j12 : float
@@ -136,13 +133,13 @@ def nnlo_decompose(gamma_singlet, j02, j12, j22):
numpy.ndarray
singlet next-to-next-to-leading order decompose EKO
"""
- return ad.exp_singlet(
+ return ad.exp_matrix_2D(
gamma_singlet[0] * j02 + gamma_singlet[1] * j12 + gamma_singlet[2] * j22
)[0]
@nb.njit(cache=True)
-def nnlo_decompose_exact(gamma_singlet, a1, a0, nf):
+def nnlo_decompose_exact(gamma_singlet, a1, a0, beta):
"""Singlet next-to-next-to-leading order decompose-exact EKO.
Parameters
@@ -153,24 +150,26 @@ def nnlo_decompose_exact(gamma_singlet, a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
Returns
-------
numpy.ndarray
singlet next-to-next-to-leading order decompose-exact EKO
"""
+ beta0 = beta[0]
+ b_vec = [betas / beta0 for betas in beta]
return nnlo_decompose(
gamma_singlet,
- ei.j02_exact(a1, a0, nf),
- ei.j12_exact(a1, a0, nf),
- ei.j22_exact(a1, a0, nf),
+ ei.j14_exact(a1, a0, beta0, b_vec),
+ ei.j24_exact(a1, a0, beta0, b_vec),
+ ei.j34_exact(a1, a0, beta0, b_vec),
)
@nb.njit(cache=True)
-def nnlo_decompose_expanded(gamma_singlet, a1, a0, nf):
+def nnlo_decompose_expanded(gamma_singlet, a1, a0, beta):
"""Singlet next-to-next-to-leading order decompose-expanded EKO.
Parameters
@@ -181,19 +180,21 @@ def nnlo_decompose_expanded(gamma_singlet, a1, a0, nf):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
Returns
-------
numpy.ndarray
singlet next-to-next-to-leading order decompose-expanded EKO
"""
+ beta0 = beta[0]
+ b_vec = [betas / beta0 for betas in beta]
return nnlo_decompose(
gamma_singlet,
- ei.j02_expanded(a1, a0, nf),
- ei.j12_expanded(a1, a0, nf),
- ei.j22_expanded(a1, a0, nf),
+ ei.j14_expanded(a1, a0, beta0, b_vec),
+ ei.j24_expanded(a1, a0, beta0, b_vec),
+ ei.j34_expanded(a1, a0, beta0),
)
@@ -205,12 +206,6 @@ def n3lo_decompose(gamma_singlet, j03, j13, j23, j33):
----------
gamma_singlet : numpy.ndarray
singlet anomalous dimensions matrices
- a1 : float
- target coupling value
- a0 : float
- initial coupling value
- nf : int
- number of active flavors
j03 : float
|LO|-|N3LO| evolution integral
j13 : float
@@ -225,7 +220,7 @@ def n3lo_decompose(gamma_singlet, j03, j13, j23, j33):
numpy.ndarray
singlet |N3LO| decompose EKO
"""
- return ad.exp_singlet(
+ return ad.exp_matrix_2D(
gamma_singlet[0] * j03
+ gamma_singlet[1] * j13
+ gamma_singlet[2] * j23
@@ -260,12 +255,12 @@ def n3lo_decompose_exact(gamma_singlet, a1, a0, nf):
beta.b_qcd((5, 0), nf),
]
roots = as4_ei.roots(b_list)
- j00 = ei.j00(a1, a0, nf)
+ j12 = ei.j12(a1, a0, beta0)
j13 = as4_ei.j13_exact(a1, a0, beta0, b_list, roots)
j23 = as4_ei.j23_exact(a1, a0, beta0, b_list, roots)
j33 = as4_ei.j33_exact(a1, a0, beta0, b_list, roots)
return n3lo_decompose(
- gamma_singlet, as4_ei.j03_exact(j00, j13, j23, j33, b_list), j13, j23, j33
+ gamma_singlet, as4_ei.j03_exact(j12, j13, j23, j33, b_list), j13, j23, j33
)
@@ -295,17 +290,17 @@ def n3lo_decompose_expanded(gamma_singlet, a1, a0, nf):
beta.b_qcd((4, 0), nf),
beta.b_qcd((5, 0), nf),
]
- j00 = ei.j00(a1, a0, nf)
+ j12 = ei.j12(a1, a0, nf)
j13 = as4_ei.j13_expanded(a1, a0, beta0, b_list)
j23 = as4_ei.j23_expanded(a1, a0, beta0, b_list)
j33 = as4_ei.j33_expanded(a1, a0, beta0)
return n3lo_decompose(
- gamma_singlet, as4_ei.j03_expanded(j00, j13, j23, j33, b_list), j13, j23, j33
+ gamma_singlet, as4_ei.j03_expanded(j12, j13, j23, j33, b_list), j13, j23, j33
)
@nb.njit(cache=True)
-def eko_iterate(gamma_singlet, a1, a0, nf, order, ev_op_iterations):
+def eko_iterate(gamma_singlet, a1, a0, beta_vec, order, ev_op_iterations):
"""Singlet |NLO|, |NNLO| or |N3LO| iterated (exact) EKO.
Parameters
@@ -316,8 +311,8 @@ def eko_iterate(gamma_singlet, a1, a0, nf, order, ev_op_iterations):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta_vec : list
+ list of the values of the beta functions
order : tuple(int,int)
perturbative order
ev_op_iterations : int
@@ -329,11 +324,6 @@ def eko_iterate(gamma_singlet, a1, a0, nf, order, ev_op_iterations):
singlet iterated (exact) EKO
"""
a_steps = utils.geomspace(a0, a1, 1 + ev_op_iterations)
- beta_vec = [beta.beta_qcd((2, 0), nf), beta.beta_qcd((3, 0), nf)]
- if order[0] >= 3:
- beta_vec.append(beta.beta_qcd((4, 0), nf))
- if order[0] >= 4:
- beta_vec.append(beta.beta_qcd((5, 0), nf))
e = np.identity(2, np.complex_)
al = a_steps[0]
for ah in a_steps[1:]:
@@ -345,14 +335,14 @@ def eko_iterate(gamma_singlet, a1, a0, nf, order, ev_op_iterations):
gamma_summed += gamma_singlet[i] * a_half**i
beta_summed += beta_vec[i] * a_half ** (i + 1)
ln = gamma_summed / beta_summed * delta_a
- ek = np.ascontiguousarray(ad.exp_singlet(ln)[0])
+ ek = np.ascontiguousarray(ad.exp_matrix_2D(ln)[0])
e = ek @ e
al = ah
return e
@nb.njit(cache=True)
-def r_vec(gamma_singlet, nf, ev_op_max_order, order, is_exact):
+def r_vec(gamma_singlet, beta, ev_op_max_order, order, is_exact):
r"""Compute singlet R vector for perturbative mode.
.. math::
@@ -363,8 +353,8 @@ def r_vec(gamma_singlet, nf, ev_op_max_order, order, is_exact):
----------
gamma_singlet : numpy.ndarray
singlet anomalous dimensions matrices
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
ev_op_max_order : tuple(int,int)
perturbative expansion order of U
order : tuple(int,int)
@@ -380,17 +370,17 @@ def r_vec(gamma_singlet, nf, ev_op_max_order, order, is_exact):
r = np.zeros(
(ev_op_max_order[0] + 1, 2, 2), dtype=np.complex_
) # k = 0 .. max_order
- beta0 = beta.beta_qcd((2, 0), nf)
+ beta0 = beta[0]
# fill explicit elements
r[0] = gamma_singlet[0] / beta0
if order[0] > 1:
- b1 = beta.b_qcd((3, 0), nf)
+ b1 = beta[1] / beta0
r[1] = gamma_singlet[1] / beta0 - b1 * r[0]
if order[0] > 2:
- b2 = beta.b_qcd((4, 0), nf)
+ b2 = beta[2] / beta0
r[2] = gamma_singlet[2] / beta0 - b1 * r[1] - b2 * r[0]
if order[0] > 3:
- b3 = beta.b_qcd((5, 0), nf)
+ b3 = beta[3] / beta0
r[3] = gamma_singlet[3] / beta0 - b1 * r[2] - b2 * r[1] - b3 * r[0]
# fill rest
if is_exact:
@@ -429,7 +419,7 @@ def u_vec(r, ev_op_max_order):
u = np.zeros((ev_op_max_order[0], 2, 2), np.complex_) # k = 0 .. max_order
# init
u[0] = np.identity(2, np.complex_)
- _, r_p, r_m, e_p, e_m = ad.exp_singlet(r[0])
+ _, r_p, r_m, e_p, e_m = ad.exp_matrix_2D(r[0])
e_p = np.ascontiguousarray(e_p)
e_m = np.ascontiguousarray(e_m)
for kk in range(1, ev_op_max_order[0]):
@@ -478,7 +468,7 @@ def sum_u(uvec, a):
@nb.njit(cache=True)
def eko_perturbative(
- gamma_singlet, a1, a0, nf, order, ev_op_iterations, ev_op_max_order, is_exact
+ gamma_singlet, a1, a0, beta, order, ev_op_iterations, ev_op_max_order, is_exact
):
"""Singlet |NLO|,|NNLO| or |N3LO| perturbative EKO, depending on which r is passed.
@@ -490,8 +480,8 @@ def eko_perturbative(
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
order : tuple(int,int)
perturbative order
ev_op_iterations : int
@@ -506,14 +496,14 @@ def eko_perturbative(
numpy.ndarray
singlet perturbative EKO
"""
- r = r_vec(gamma_singlet, nf, ev_op_max_order, order, is_exact)
+ r = r_vec(gamma_singlet, beta, ev_op_max_order, order, is_exact)
uk = u_vec(r, ev_op_max_order)
e = np.identity(2, np.complex_)
# iterate elements
a_steps = utils.geomspace(a0, a1, 1 + ev_op_iterations)
al = a_steps[0]
for ah in a_steps[1:]:
- e0 = lo_exact(gamma_singlet, ah, al, nf)
+ e0 = lo_exact(gamma_singlet, ah, al, beta)
uh = sum_u(uk, ah)
ul = sum_u(uk, al)
# join elements
@@ -524,7 +514,7 @@ def eko_perturbative(
@nb.njit(cache=True)
-def eko_truncated(gamma_singlet, a1, a0, nf, order, ev_op_iterations):
+def eko_truncated(gamma_singlet, a1, a0, beta, order, ev_op_iterations):
"""Singlet |NLO|, |NNLO| or |N3LO| truncated EKO.
Parameters
@@ -535,8 +525,8 @@ def eko_truncated(gamma_singlet, a1, a0, nf, order, ev_op_iterations):
target coupling value
a0 : float
initial coupling value
- nf : int
- number of active flavors
+ beta : list
+ list of the values of the beta functions
order : tuple(int,int)
perturbative order
ev_op_iterations : int
@@ -547,7 +537,7 @@ def eko_truncated(gamma_singlet, a1, a0, nf, order, ev_op_iterations):
numpy.ndarray
singlet truncated EKO
"""
- r = r_vec(gamma_singlet, nf, order, order, False)
+ r = r_vec(gamma_singlet, beta, order, order, False)
u = u_vec(r, order)
u1 = np.ascontiguousarray(u[1])
e = np.identity(2, np.complex_)
@@ -555,7 +545,7 @@ def eko_truncated(gamma_singlet, a1, a0, nf, order, ev_op_iterations):
a_steps = utils.geomspace(a0, a1, 1 + ev_op_iterations)
al = a_steps[0]
for ah in a_steps[1:]:
- e0 = np.ascontiguousarray(lo_exact(gamma_singlet, ah, al, nf))
+ e0 = np.ascontiguousarray(lo_exact(gamma_singlet, ah, al, beta))
if order[0] >= 2:
ek = e0 + ah * u1 @ e0 - al * e0 @ u1
if order[0] >= 3:
@@ -610,38 +600,53 @@ def dispatcher( # pylint: disable=too-many-return-statements
numpy.ndarray
singlet EKO
"""
+ betalist = [beta.beta_qcd((2 + i, 0), nf) for i in range(order[0])]
# for SV expanded we still fall in here, but we don't need to do anything
if a1 == a0:
return np.eye(len(gamma_singlet[0]), dtype=np.complex_)
# use always exact in LO
if order[0] == 1:
- return lo_exact(gamma_singlet, a1, a0, nf)
+ return lo_exact(gamma_singlet, a1, a0, betalist)
# Common method for NLO and NNLO
if method in ["iterate-exact", "iterate-expanded"]:
- return eko_iterate(gamma_singlet, a1, a0, nf, order, ev_op_iterations)
+ return eko_iterate(gamma_singlet, a1, a0, betalist, order, ev_op_iterations)
if method == "perturbative-exact":
return eko_perturbative(
- gamma_singlet, a1, a0, nf, order, ev_op_iterations, ev_op_max_order, True
+ gamma_singlet,
+ a1,
+ a0,
+ betalist,
+ order,
+ ev_op_iterations,
+ ev_op_max_order,
+ True,
)
if method == "perturbative-expanded":
return eko_perturbative(
- gamma_singlet, a1, a0, nf, order, ev_op_iterations, ev_op_max_order, False
+ gamma_singlet,
+ a1,
+ a0,
+ betalist,
+ order,
+ ev_op_iterations,
+ ev_op_max_order,
+ False,
)
if method in ["truncated", "ordered-truncated"]:
- return eko_truncated(gamma_singlet, a1, a0, nf, order, ev_op_iterations)
+ return eko_truncated(gamma_singlet, a1, a0, betalist, order, ev_op_iterations)
# These methods are scattered for nlo and nnlo
if method == "decompose-exact":
if order[0] == 2:
- return nlo_decompose_exact(gamma_singlet, a1, a0, nf)
+ return nlo_decompose_exact(gamma_singlet, a1, a0, betalist)
elif order[0] == 3:
- return nnlo_decompose_exact(gamma_singlet, a1, a0, nf)
+ return nnlo_decompose_exact(gamma_singlet, a1, a0, betalist)
return n3lo_decompose_exact(gamma_singlet, a1, a0, nf)
if method == "decompose-expanded":
if order[0] == 2:
- return nlo_decompose_expanded(gamma_singlet, a1, a0, nf)
+ return nlo_decompose_expanded(gamma_singlet, a1, a0, betalist)
elif order[0] == 3:
- return nnlo_decompose_expanded(gamma_singlet, a1, a0, nf)
+ return nnlo_decompose_expanded(gamma_singlet, a1, a0, betalist)
return n3lo_decompose_expanded(gamma_singlet, a1, a0, nf)
raise NotImplementedError("Selected method is not implemented")
diff --git a/src/eko/kernels/singlet_qed.py b/src/eko/kernels/singlet_qed.py
new file mode 100644
index 000000000..e85ad5112
--- /dev/null
+++ b/src/eko/kernels/singlet_qed.py
@@ -0,0 +1,103 @@
+"""Collection of QED singlet EKOs."""
+import numba as nb
+import numpy as np
+
+from ekore import anomalous_dimensions as ad
+
+from .. import beta
+from . import utils
+
+
+@nb.njit(cache=True)
+def eko_iterate(gamma_singlet, as_list, a_half, nf, order, ev_op_iterations, dim):
+ """Singlet QEDxQCD iterated (exact) EKO.
+
+ Parameters
+ ----------
+ gamma_singlet : numpy.ndarray
+ singlet anomalous dimensions matrices
+ a1 : float
+ target strong coupling value
+ a0 : float
+ initial strong coupling value
+ aem_list : float
+ electromagnetic coupling values
+ nf : int
+ number of active flavors
+ order : tuple(int,int)
+ QCDxQED perturbative orders
+ ev_op_iterations : int
+ number of evolution steps
+
+ Returns
+ -------
+ e_s^{order} : numpy.ndarray
+ singlet QEDxQCD iterated (exact) EKO
+ """
+ e = np.identity(dim, np.complex_)
+ betaQCD = np.zeros((order[0] + 1, order[1] + 1))
+ for i in range(1, order[0] + 1):
+ betaQCD[i, 0] = beta.beta_qcd((i + 1, 0), nf)
+ betaQCD[1, 1] = beta.beta_qcd((2, 1), nf)
+ for step in range(1, ev_op_iterations + 1):
+ ah = as_list[step]
+ al = as_list[step - 1]
+ as_half = a_half[step - 1, 0]
+ aem = a_half[step - 1, 1]
+ delta_a = ah - al
+ gamma = np.zeros((dim, dim), np.complex_)
+ betatot = 0
+ for i in range(0, order[0] + 1):
+ for j in range(0, order[1] + 1):
+ betatot += betaQCD[i, j] * as_half ** (i + 1) * aem**j
+ gamma += gamma_singlet[i, j] * as_half**i * aem**j
+ ln = gamma / betatot * delta_a
+ ek = np.ascontiguousarray(ad.exp_matrix(ln)[0])
+ e = ek @ e
+ return e
+
+
+@nb.njit(cache=True)
+def dispatcher(
+ order,
+ method,
+ gamma_singlet,
+ as_list,
+ a_half,
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+):
+ """Determine used kernel and call it.
+
+ Parameters
+ ----------
+ order : tuple(int,int)
+ perturbative order
+ method : str
+ method
+ gamma_singlet : numpy.ndarray
+ singlet anomalous dimensions matrices
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ aem_list : numpy.ndarray
+ electromagnetic coupling values
+ nf : int
+ number of active flavors
+ ev_op_iterations : int
+ number of evolution steps
+ ev_op_max_order : tuple(int,int)
+ perturbative expansion order of U
+
+ Returns
+ -------
+ e_s : numpy.ndarray
+ singlet EKO
+ """
+ if method in ["iterate-exact", "iterate-expanded"]:
+ return eko_iterate(
+ gamma_singlet, as_list, a_half, nf, order, ev_op_iterations, 4
+ )
+ raise NotImplementedError('Only "iterate-exact" is implemented with QED')
diff --git a/src/eko/kernels/valence_qed.py b/src/eko/kernels/valence_qed.py
new file mode 100644
index 000000000..3a8482f68
--- /dev/null
+++ b/src/eko/kernels/valence_qed.py
@@ -0,0 +1,51 @@
+"""Collection of QED valence EKOs."""
+import numba as nb
+
+from .singlet_qed import eko_iterate
+
+
+@nb.njit(cache=True)
+def dispatcher(
+ order,
+ method,
+ gamma_valence,
+ as_list,
+ a_half,
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+):
+ """
+ Determine used kernel and call it.
+
+ Parameters
+ ----------
+ order : tuple(int,int)
+ perturbative order
+ method : str
+ method
+ gamma_singlet : numpy.ndarray
+ singlet anomalous dimensions matrices
+ a1 : float
+ target coupling value
+ a0 : float
+ initial coupling value
+ aem_list : numpy.ndarray
+ electromagnetic coupling values
+ nf : int
+ number of active flavors
+ ev_op_iterations : int
+ number of evolution steps
+ ev_op_max_order : tuple(int,int)
+ perturbative expansion order of U
+
+ Returns
+ -------
+ e_v : numpy.ndarray
+ singlet EKO
+ """
+ if method in ["iterate-exact", "iterate-expanded"]:
+ return eko_iterate(
+ gamma_valence, as_list, a_half, nf, order, ev_op_iterations, 2
+ )
+ raise NotImplementedError('Only "iterate-exact" is implemented with QED')
diff --git a/src/eko/runner/legacy.py b/src/eko/runner/legacy.py
index accc4dc37..133653dc4 100644
--- a/src/eko/runner/legacy.py
+++ b/src/eko/runner/legacy.py
@@ -107,7 +107,7 @@ def __init__(
configs=new_operator.configs,
debug=new_operator.debug,
thresholds_config=tc,
- strong_coupling=sc,
+ couplings=sc,
interpol_dispatcher=bfd,
)
diff --git a/src/eko/scale_variations/expanded.py b/src/eko/scale_variations/expanded.py
index 3ceb5c6d4..08b752a09 100644
--- a/src/eko/scale_variations/expanded.py
+++ b/src/eko/scale_variations/expanded.py
@@ -135,7 +135,7 @@ def non_singlet_variation(gamma, a_s, order, nf, L):
@nb.njit(cache=True)
-def singlet_variation(gamma, a_s, order, nf, L):
+def singlet_variation(gamma, a_s, order, nf, L, dim):
"""Singlet scale variation dispatcher.
Parameters
@@ -156,7 +156,7 @@ def singlet_variation(gamma, a_s, order, nf, L):
numpy.ndarray
scale variation kernel
"""
- sv_ker = np.eye(2, dtype=np.complex_)
+ sv_ker = np.eye(dim, dtype=np.complex_)
gamma = np.ascontiguousarray(gamma)
if order[0] >= 2:
sv_ker -= a_s * variation_as1(gamma, L)
@@ -174,3 +174,90 @@ def singlet_variation(gamma, a_s, order, nf, L):
gamma, L, beta0, beta1, gamma0e2, gamma0e3, g1g0, g0g1
)
return sv_ker
+
+
+@nb.njit(cache=True)
+def non_singlet_variation_qed(gamma, a_s, a_em, alphaem_running, order, nf, L):
+ """Non-singlet scale variation dispatcher.
+
+ Parameters
+ ----------
+ gamma : numpy.ndarray
+ anomalous dimensions
+ a_s : float
+ target coupling value
+ order : int
+ perturbation order
+ nf : int
+ number of active flavors
+ L : float
+ logarithmic ratio of factorization and renormalization scale
+
+ Returns
+ -------
+ complex
+ scale variation kernel
+ """
+ sv_ker = non_singlet_variation(gamma[1:, 0], a_s, order, nf, L)
+ if alphaem_running:
+ if order[1] >= 2:
+ sv_ker -= a_em * variation_as1(gamma[0, 1:], L)
+ return sv_ker
+
+
+@nb.njit(cache=True)
+def singlet_variation_qed(gamma, a_s, a_em, alphaem_running, order, nf, L):
+ """Singlet scale variation dispatcher.
+
+ Parameters
+ ----------
+ gamma : numpy.ndarray
+ anomalous dimensions
+ a_s : float
+ target coupling value
+ order : int
+ perturbation order
+ nf : int
+ number of active flavors
+ L : float
+ logarithmic ratio of factorization and renormalization scale
+
+ Returns
+ -------
+ numpy.ndarray
+ scale variation kernel
+ """
+ sv_ker = singlet_variation(gamma[1:, 0], a_s, order, nf, L, 4)
+ if alphaem_running:
+ if order[1] >= 2:
+ sv_ker -= a_em * variation_as1(gamma[0, 1:], L)
+ return sv_ker
+
+
+@nb.njit(cache=True)
+def valence_variation_qed(gamma, a_s, a_em, alphaem_running, order, nf, L):
+ """Singlet scale variation dispatcher.
+
+ Parameters
+ ----------
+ gamma : numpy.ndarray
+ anomalous dimensions
+ a_s : float
+ target coupling value
+ order : int
+ perturbation order
+ nf : int
+ number of active flavors
+ L : float
+ logarithmic ratio of factorization and renormalization scale
+
+ Returns
+ -------
+ numpy.ndarray
+ scale variation kernel
+ """
+ sv_ker = singlet_variation(gamma[1:, 0], a_s, order, nf, L, 2)
+ if alphaem_running:
+ if order[1] >= 2:
+ sv_ker -= a_em * variation_as1(gamma[0, 1:], L)
+ return sv_ker
diff --git a/src/eko/scale_variations/exponentiated.py b/src/eko/scale_variations/exponentiated.py
index c1a2e9316..c8b943e8b 100644
--- a/src/eko/scale_variations/exponentiated.py
+++ b/src/eko/scale_variations/exponentiated.py
@@ -43,3 +43,34 @@ def gamma_variation(gamma, order, nf, L):
if order[0] >= 2:
gamma[1] -= beta0 * gamma[0] * L
return gamma
+
+
+@nb.njit(cache=True)
+def gamma_variation_qed(gamma, order, nf, L, alphaem_running):
+ """Adjust the anomalous dimensions with the scale variations.
+
+ Parameters
+ ----------
+ gamma : numpy.ndarray
+ anomalous dimensions
+ order : tuple(int,int)
+ perturbation order
+ nf : int
+ number of active flavors
+ L : float
+ logarithmic ratio of factorization and renormalization scale
+
+ Returns
+ -------
+ gamma : numpy.ndarray
+ adjusted anomalous dimensions
+ """
+ # if alphaem is fixed then only alphas is varied so gamma[0,1] and gamma[0,2]
+ # don't get a variation while gamma[1,1] gets a variation that is O(as2aem1)
+ # that we are neglecting
+ gamma[1:, 0] = gamma_variation(gamma[1:, 0], order, nf, L)
+ if alphaem_running:
+ if order[1] >= 2:
+ beta0qed = beta.beta_qed((0, 2), nf)
+ gamma[0, 2] -= beta0qed * gamma[0, 1] * L
+ return gamma
diff --git a/src/ekomark/apply.py b/src/ekomark/apply.py
index 827850f7c..3c1f2cbe6 100644
--- a/src/ekomark/apply.py
+++ b/src/ekomark/apply.py
@@ -7,7 +7,13 @@
from eko.io import EKO
-def apply_pdf(eko: EKO, lhapdf_like, targetgrid=None, rotate_to_evolution_basis=False):
+def apply_pdf(
+ eko: EKO,
+ lhapdf_like,
+ targetgrid=None,
+ rotate_to_evolution_basis=False,
+ qed=False,
+):
"""
Apply all available operators to the input PDFs.
@@ -29,8 +35,12 @@ def apply_pdf(eko: EKO, lhapdf_like, targetgrid=None, rotate_to_evolution_basis=
output PDFs and their associated errors for the computed Q2grid
"""
if rotate_to_evolution_basis:
+ if not qed:
+ rotate_flavor_to_evolution = br.rotate_flavor_to_evolution
+ else:
+ rotate_flavor_to_evolution = br.rotate_flavor_to_unified_evolution
return apply_pdf_flavor(
- eko, lhapdf_like, targetgrid, br.rotate_flavor_to_evolution
+ eko, lhapdf_like, targetgrid, rotate_flavor_to_evolution, qed
)
return apply_pdf_flavor(eko, lhapdf_like, targetgrid)
@@ -38,7 +48,9 @@ def apply_pdf(eko: EKO, lhapdf_like, targetgrid=None, rotate_to_evolution_basis=
CONTRACTION = "ajbk,bk"
-def apply_pdf_flavor(eko: EKO, lhapdf_like, targetgrid=None, flavor_rotation=None):
+def apply_pdf_flavor(
+ eko: EKO, lhapdf_like, targetgrid=None, flavor_rotation=None, qed=False
+):
"""
Apply all available operators to the input PDFs.
@@ -53,6 +65,8 @@ def apply_pdf_flavor(eko: EKO, lhapdf_like, targetgrid=None, flavor_rotation=Non
if given, interpolates to the pdfs given at targetgrid (instead of xgrid)
flavor_rotation : np.ndarray
Rotation matrix in flavor space
+ qed : bool
+ activate qed
Returns
-------
@@ -92,12 +106,16 @@ def apply_pdf_flavor(eko: EKO, lhapdf_like, targetgrid=None, flavor_rotation=Non
pdf = flavor_rotation @ np.array(
[op["pdfs"][pid] for pid in br.flavor_basis_pids]
)
- op["pdfs"] = dict(zip(br.evol_basis, pdf))
+ if not qed:
+ evol_basis = br.evol_basis
+ else:
+ evol_basis = br.unified_evol_basis
+ op["pdfs"] = dict(zip(evol_basis, pdf))
if op["errors"] is not None:
errors = flavor_rotation @ np.array(
[op["errors"][pid] for pid in br.flavor_basis_pids]
)
- op["errors"] = dict(zip(br.evol_basis, errors))
+ op["errors"] = dict(zip(evol_basis, errors))
# rotate/interpolate to target grid
if targetgrid is not None:
diff --git a/src/ekomark/benchmark/external/apfel_utils.py b/src/ekomark/benchmark/external/apfel_utils.py
index 8b871b89e..d2010a5a2 100644
--- a/src/ekomark/benchmark/external/apfel_utils.py
+++ b/src/ekomark/benchmark/external/apfel_utils.py
@@ -73,20 +73,21 @@ def compute_apfel_data(
# Run
apf_tabs = {}
for q2 in operators["Q2grid"]:
-
apfel.EvolveAPFEL(theory["Q0"], np.sqrt(q2))
print(f"Executing APFEL took {(time.perf_counter() - apf_start)} s")
tab = {}
for pid in br.flavor_basis_pids:
-
if pid in skip_pdfs:
continue
# collect APFEL
apf = []
for x in target_xgrid:
- xf = apfel.xPDF(pid if pid != 21 else 0, x)
+ if pid != 22:
+ xf = apfel.xPDF(pid if pid != 21 else 0, x)
+ else:
+ xf = apfel.xgamma(x)
# if pid == 4:
# print(pid,x,xf)
apf.append(xf)
@@ -94,14 +95,21 @@ def compute_apfel_data(
# rotate if needed
if rotate_to_evolution_basis:
+ qed = theory["QED"] > 0
+ if not qed:
+ evol_basis = br.evol_basis
+ rotate_flavor_to_evolution = br.rotate_flavor_to_evolution
+ else:
+ evol_basis = br.unified_evol_basis
+ rotate_flavor_to_evolution = br.rotate_flavor_to_unified_evolution
pdfs = np.array(
[
tab[pid] if pid in tab else np.zeros(len(target_xgrid))
for pid in br.flavor_basis_pids
]
)
- evol_pdf = br.rotate_flavor_to_evolution @ pdfs
- tab = dict(zip(br.evol_basis, evol_pdf))
+ evol_pdf = rotate_flavor_to_evolution @ pdfs
+ tab = dict(zip(evol_basis, evol_pdf))
apf_tabs[q2] = tab
diff --git a/src/ekomark/benchmark/external/lhapdf_utils.py b/src/ekomark/benchmark/external/lhapdf_utils.py
index 669563dea..e337d8476 100644
--- a/src/ekomark/benchmark/external/lhapdf_utils.py
+++ b/src/ekomark/benchmark/external/lhapdf_utils.py
@@ -4,7 +4,9 @@
from eko import basis_rotation as br
-def compute_LHAPDF_data(operators, pdf, skip_pdfs, rotate_to_evolution_basis=False):
+def compute_LHAPDF_data(
+ theory, operators, pdf, skip_pdfs, rotate_to_evolution_basis=False
+):
"""
Run LHAPDF to compute operators.
@@ -43,14 +45,21 @@ def compute_LHAPDF_data(operators, pdf, skip_pdfs, rotate_to_evolution_basis=Fal
# rotate if needed
if rotate_to_evolution_basis:
+ qed = theory["QED"] > 0
+ if not qed:
+ evol_basis = br.evol_basis
+ rotate_flavor_to_evolution = br.rotate_flavor_to_evolution
+ else:
+ evol_basis = br.unified_evol_basis
+ rotate_flavor_to_evolution = br.rotate_flavor_to_unified_evolution
pdfs = np.array(
[
tab[pid] if pid in tab else np.zeros(len(target_xgrid))
for pid in br.flavor_basis_pids
]
)
- evol_pdf = br.rotate_flavor_to_evolution @ pdfs
- tab = dict(zip(br.evol_basis, evol_pdf))
+ evol_pdf = rotate_flavor_to_evolution @ pdfs
+ tab = dict(zip(evol_basis, evol_pdf))
out_tabs[q2] = tab
diff --git a/src/ekomark/benchmark/external/pegasus_utils.py b/src/ekomark/benchmark/external/pegasus_utils.py
index d3b0f28ac..fecb21caf 100644
--- a/src/ekomark/benchmark/external/pegasus_utils.py
+++ b/src/ekomark/benchmark/external/pegasus_utils.py
@@ -87,7 +87,6 @@ def compute_pegasus_data(theory, operators, skip_pdfs, rotate_to_evolution_basis
# run pegaus
out_tabs = {}
for q2 in operators["Q2grid"]:
-
tab = {}
for x in target_xgrid:
# last two numbers are the min and max pid to calculate,
@@ -103,14 +102,21 @@ def compute_pegasus_data(theory, operators, skip_pdfs, rotate_to_evolution_basis
# rotate if needed
if rotate_to_evolution_basis:
+ qed = theory["QED"] > 0
+ if not qed:
+ evol_basis = br.evol_basis
+ rotate_flavor_to_evolution = br.rotate_flavor_to_evolution
+ else:
+ evol_basis = br.unified_evol_basis
+ rotate_flavor_to_evolution = br.rotate_flavor_to_unified_evolution
pdfs = np.array(
[
tab[pid] if pid in tab else np.zeros(len(target_xgrid))
for pid in br.flavor_basis_pids
]
)
- evol_pdf = br.rotate_flavor_to_evolution @ pdfs
- tab = dict(zip(br.evol_basis, evol_pdf))
+ evol_pdf = rotate_flavor_to_evolution @ pdfs
+ tab = dict(zip(evol_basis, evol_pdf))
out_tabs[q2] = tab
diff --git a/src/ekomark/benchmark/runner.py b/src/ekomark/benchmark/runner.py
index 05e8b75c4..f09bc608d 100644
--- a/src/ekomark/benchmark/runner.py
+++ b/src/ekomark/benchmark/runner.py
@@ -1,6 +1,5 @@
-"""
-Abstract layer for running the benchmarks
-"""
+"""Abstract layer for running the benchmarks."""
+
import functools
import logging
import os
@@ -21,9 +20,7 @@
class Runner(BenchmarkRunner):
- """
- EKO specialization of the banana runner.
- """
+ """EKO specialization of the banana runner."""
db_base_cls = db.Base
rotate_to_evolution_basis = False
@@ -35,15 +32,31 @@ def __init__(self):
@staticmethod
def load_ocards(session, ocard_updates):
+ """
+ Load operator cards.
+
+ Parameters
+ ----------
+ session : sqlalchemy.session.Session
+ DB ORM session
+ updates : dict
+ modifiers
+
+ Returns
+ -------
+ cards : list(dict)
+ list of records
+ """
return operators.load(session, ocard_updates)
@staticmethod
def skip_pdfs(_theory):
+ """Specify PDFs to skip."""
return []
def run_me(self, theory, ocard, _pdf):
"""
- Run eko
+ Run eko.
Parameters
----------
@@ -59,7 +72,6 @@ def run_me(self, theory, ocard, _pdf):
out : dict
DGLAP result
"""
-
# activate logging
logStdout = logging.StreamHandler(sys.stdout)
logStdout.setLevel(logging.INFO)
@@ -104,7 +116,11 @@ def run_me(self, theory, ocard, _pdf):
with EKO.read(path) as out_copy:
change_lab = False
if self.rotate_to_evolution_basis:
- manipulate.to_evol(out_copy, source=True, target=True)
+ qed = theory["order"][1] > 0
+ if not qed:
+ manipulate.to_evol(out_copy, source=True, target=True)
+ else:
+ manipulate.to_uni_evol(out_copy, source=True, target=True)
change_lab = True
save_operators_to_pdf(
@@ -122,6 +138,23 @@ def run_me(self, theory, ocard, _pdf):
return path
def run_external(self, theory, ocard, pdf):
+ """
+ Run external library.
+
+ Parameters
+ ----------
+ theory : dict
+ theory card
+ ocard : dict
+ operator card
+ pdf : lhapdf_type
+ pdf
+
+ Returns
+ -------
+ out : dict
+ DGLAP result
+ """
# pylint:disable=import-error,import-outside-toplevel
if self.external.lower() == "lha":
from .external import LHA_utils
@@ -137,6 +170,7 @@ def run_external(self, theory, ocard, pdf):
# here theory card is not needed
return lhapdf_utils.compute_LHAPDF_data(
+ theory,
ocard,
pdf,
self.skip_pdfs(theory),
@@ -168,6 +202,19 @@ def run_external(self, theory, ocard, pdf):
)
def log(self, theory, _, pdf, me, ext):
+ """
+ Return a proper log table.
+
+ Parameters
+ ----------
+ theory : dict
+ theory card
+ pdf : lhapdf_type
+ pdf
+ me : eko.output.Output
+ eko output object containing all informations
+
+ """
# return a proper log table
log_tabs = {}
xgrid = ext["target_xgrid"]
@@ -176,8 +223,12 @@ def log(self, theory, _, pdf, me, ext):
# LHA NNLO VFNS needs a special treatment
# Valence contains only u and d
rotate_to_evolution = None
+ qed = theory["QED"] > 0
if self.rotate_to_evolution_basis:
- rotate_to_evolution = br.rotate_flavor_to_evolution.copy()
+ if not qed:
+ rotate_to_evolution = br.rotate_flavor_to_evolution.copy()
+ else:
+ rotate_to_evolution = br.rotate_flavor_to_unified_evolution.copy()
if (
self.external == "LHA"
and theory["PTO"] == 2
@@ -191,6 +242,7 @@ def log(self, theory, _, pdf, me, ext):
pdf,
xgrid,
flavor_rotation=rotate_to_evolution,
+ qed=qed,
)
for q2 in q2s:
diff --git a/src/ekore/anomalous_dimensions/__init__.py b/src/ekore/anomalous_dimensions/__init__.py
index 47a877c03..254070755 100644
--- a/src/ekore/anomalous_dimensions/__init__.py
+++ b/src/ekore/anomalous_dimensions/__init__.py
@@ -21,9 +21,9 @@
@nb.njit(cache=True)
-def exp_singlet(gamma_S):
+def exp_matrix_2D(gamma_S):
r"""
- Computes the exponential and the eigensystem of the singlet anomalous dimension matrix
+ Compute the exponential and the eigensystem of the singlet anomalous dimension matrix.
Parameters
----------
@@ -60,3 +60,38 @@ def exp_singlet(gamma_S):
e_m = -c * (gamma_S - lambda_p * identity)
exp = e_m * np.exp(lambda_m) + e_p * np.exp(lambda_p)
return exp, lambda_p, lambda_m, e_p, e_m
+
+
+@nb.njit(cache=True)
+def exp_matrix(gamma):
+ r"""
+ Compute the exponential and the eigensystem of a matrix.
+
+ Parameters
+ ----------
+ gamma : numpy.ndarray
+ input matrix
+
+ Returns
+ -------
+ exp : numpy.ndarray
+ exponential of the matrix gamma :math:`\gamma(N)`
+ w : numpy.ndarray
+ array of the eigenvalues of the matrix lambda
+ e : numpy.ndarray
+ projectors on the eigenspaces of the matrix gamma :math:`\gamma(N)`
+ """
+ dim = gamma.shape[0]
+ e = np.zeros((dim, dim, dim), np.complex_)
+ # if dim == 2:
+ # exp, lambda_p, lambda_m, e_p, e_m = exp_matrix_2D(gamma)
+ # e[0] = e_p
+ # e[1] = e_m
+ # return exp, np.array([lambda_p, lambda_m]), e
+ w, v = np.linalg.eig(gamma)
+ v_inv = np.linalg.inv(v)
+ exp = np.zeros((dim, dim), np.complex_)
+ for i in range(dim):
+ e[i] = np.outer(v[:, i], v_inv[i])
+ exp += e[i] * np.exp(w[i])
+ return exp, w, e
diff --git a/src/ekore/anomalous_dimensions/unpolarized/space_like/__init__.py b/src/ekore/anomalous_dimensions/unpolarized/space_like/__init__.py
index dc4ad9dbf..9ae3398d0 100644
--- a/src/ekore/anomalous_dimensions/unpolarized/space_like/__init__.py
+++ b/src/ekore/anomalous_dimensions/unpolarized/space_like/__init__.py
@@ -18,13 +18,15 @@
import numba as nb
import numpy as np
+from eko import constants
+
from .... import harmonics
-from . import aem1, aem2, as1, as2, as3, as4
+from . import aem1, aem2, as1, as1aem1, as2, as3, as4
@nb.njit(cache=True)
def gamma_ns(order, mode, n, nf):
- r"""Computes the tower of the non-singlet anomalous dimensions
+ r"""Compute the tower of the non-singlet anomalous dimensions.
Parameters
----------
@@ -104,7 +106,7 @@ def gamma_ns(order, mode, n, nf):
@nb.njit(cache=True)
def gamma_singlet(order, n, nf):
- r"""Computes the tower of the singlet anomalous dimensions matrices
+ r"""Compute the tower of the singlet anomalous dimensions matrices.
Parameters
----------
@@ -154,3 +156,256 @@ def gamma_singlet(order, n, nf):
if order[0] >= 4:
gamma_s[3] = as4.gamma_singlet(n, nf, full_sx_cache)
return gamma_s
+
+
+@nb.njit(cache=True)
+def gamma_ns_qed(order, mode, n, nf):
+ r"""
+ Compute the grid of the QED non-singlet anomalous dimensions.
+
+ Parameters
+ ----------
+ order : tuple(int,int)
+ perturbative orders
+ mode : 10102 | 10103 | 10202 | 10203
+ sector identifier
+ n : complex
+ Mellin variable
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_ns : numpy.ndarray
+ non-singlet QED anomalous dimensions
+
+ See Also
+ --------
+ eko.anomalous_dimensions.as1.gamma_ns : :math:`\gamma_{ns}^{(0)}(N)`
+ eko.anomalous_dimensions.as2.gamma_nsp : :math:`\gamma_{ns,+}^{(1)}(N)`
+ eko.anomalous_dimensions.as2.gamma_nsm : :math:`\gamma_{ns,-}^{(1)}(N)`
+ eko.anomalous_dimensions.as3.gamma_nsp : :math:`\gamma_{ns,+}^{(2)}(N)`
+ eko.anomalous_dimensions.as3.gamma_nsm : :math:`\gamma_{ns,-}^{(2)}(N)`
+ eko.anomalous_dimensions.as3.gamma_nsv : :math:`\gamma_{ns,v}^{(2)}(N)`
+ eko.anomalous_dimensions.aem1.gamma_ns : :math:`\gamma_{ns}^{(0,1)}(N)`
+ eko.anomalous_dimensions.aem1.gamma_ns : :math:`\gamma_{ns,v}^{(0,1)}(N)`
+ eko.anomalous_dimensions.as1aem1.gamma_nsp : :math:`\gamma_{ns,p}^{(1,1)}(N)`
+ eko.anomalous_dimensions.as1aem1.gamma_nsm : :math:`\gamma_{ns,m}^{(1,1)}(N)`
+ eko.anomalous_dimensions.aem2.gamma_nspu : :math:`\gamma_{ns,pu}^{(0,2)}(N)`
+ eko.anomalous_dimensions.aem2.gamma_nsmu : :math:`\gamma_{ns,mu}^{(0,2)}(N)`
+ eko.anomalous_dimensions.aem2.gamma_nspd : :math:`\gamma_{ns,pd}^{(0,2)}(N)`
+ eko.anomalous_dimensions.aem2.gamma_nsmd : :math:`\gamma_{ns,md}^{(0,2)}(N)`
+ """
+ # cache the s-es
+ max_weight = max(order)
+ if max_weight >= 3:
+ # here we need only S1,S2,S3,S4
+ sx = harmonics.sx(n, max_weight=max_weight + 1)
+ else:
+ sx = harmonics.sx(n, max_weight=3)
+ sx_ns_qed = harmonics.compute_qed_ns_cache(n, sx[0])
+ # now combine
+ gamma_ns = np.zeros((order[0] + 1, order[1] + 1), np.complex_)
+ gamma_ns[1, 0] = as1.gamma_ns(n, sx[0])
+ gamma_ns[0, 1] = choose_ns_ad_aem1(mode, n, sx)
+ gamma_ns[1, 1] = choose_ns_ad_as1aem1(mode, n, sx, sx_ns_qed)
+ # NLO and beyond
+ if order[0] >= 2:
+ if mode in [10102, 10103]:
+ gamma_ns[2, 0] = as2.gamma_nsp(n, nf, sx)
+ # To fill the full valence vector in NNLO we need to add gamma_ns^1 explicitly here
+ else:
+ gamma_ns[2, 0] = as2.gamma_nsm(n, nf, sx)
+ if order[1] >= 2:
+ gamma_ns[0, 2] = choose_ns_ad_aem2(mode, n, nf, sx, sx_ns_qed)
+ # NNLO and beyond
+ if order[0] >= 3:
+ if mode in [10102, 10103]:
+ gamma_ns[3, 0] = as3.gamma_nsp(n, nf, sx)
+ elif mode in [10202, 10203]:
+ gamma_ns[3, 0] = as3.gamma_nsm(n, nf, sx)
+ return gamma_ns
+
+
+@nb.njit(cache=True)
+def choose_ns_ad_aem1(mode, n, sx):
+ r"""
+ Select the non-singlet anomalous dimension at O(aem1) with the correct charge factor.
+
+ Parameters
+ ----------
+ mode : 10102 | 10202 | 10103 | 10203
+ sector identifier
+ n : complex
+ Mellin variable
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ """
+ if mode in [10102, 10202]:
+ return constants.eu2 * aem1.gamma_ns(n, sx)
+ elif mode in [10103, 10203]:
+ return constants.ed2 * aem1.gamma_ns(n, sx)
+ raise NotImplementedError("Non-singlet sector is not implemented")
+
+
+@nb.njit(cache=True)
+def choose_ns_ad_as1aem1(mode, n, sx, sx_ns_qed):
+ r"""
+ Select the non-singlet anomalous dimension at O(as1aem1) with the correct charge factor.
+
+ Parameters
+ ----------
+ mode : 10102 | 10202 | 10103 | 10203
+ sector identifier
+ n : complex
+ Mellin variable
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ """
+ if mode == 10102:
+ return constants.eu2 * as1aem1.gamma_nsp(n, sx, sx_ns_qed)
+ elif mode == 10103:
+ return constants.ed2 * as1aem1.gamma_nsp(n, sx, sx_ns_qed)
+ elif mode == 10202:
+ return constants.eu2 * as1aem1.gamma_nsm(n, sx, sx_ns_qed)
+ elif mode == 10203:
+ return constants.ed2 * as1aem1.gamma_nsm(n, sx, sx_ns_qed)
+ raise NotImplementedError("Non-singlet sector is not implemented")
+
+
+@nb.njit(cache=True)
+def choose_ns_ad_aem2(mode, n, nf, sx, sx_ns_qed):
+ r"""
+ Select the non-singlet anomalous dimension at O(aem2) with the correct charge factor.
+
+ Parameters
+ ----------
+ mode : 10102 | 10202 | 10103 | 10203
+ sector identifier
+ n : complex
+ Mellin variable
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_ns : numpy.ndarray
+ non-singlet anomalous dimensions
+ """
+ if mode == 10102:
+ return constants.eu2 * aem2.gamma_nspu(n, nf, sx, sx_ns_qed)
+ elif mode == 10103:
+ return constants.ed2 * aem2.gamma_nspd(n, nf, sx, sx_ns_qed)
+ elif mode == 10202:
+ return constants.eu2 * aem2.gamma_nsmu(n, nf, sx, sx_ns_qed)
+ elif mode == 10203:
+ return constants.ed2 * aem2.gamma_nsmd(n, nf, sx, sx_ns_qed)
+ raise NotImplementedError("Non-singlet sector is not implemented")
+
+
+@nb.njit(cache=True)
+def gamma_singlet_qed(order, n, nf):
+ r"""
+ Compute the grid of the QED singlet anomalous dimensions matrices.
+
+ Parameters
+ ----------
+ order : tuple(int,int)
+ perturbative orders
+ n : complex
+ Mellin variable
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_singlet : numpy.ndarray
+ singlet anomalous dimensions matrices
+
+ See Also
+ --------
+ eko.anomalous_dimensions.as1.gamma_singlet_qed : :math:`\gamma_{S}^{(0)}(N)`
+ eko.anomalous_dimensions.as2.gamma_singlet_qed : :math:`\gamma_{S}^{(1)}(N)`
+ eko.anomalous_dimensions.as3.gamma_singlet_qed : :math:`\gamma_{S}^{(2)}(N)`
+ eko.anomalous_dimensions.aem1.gamma_singlet : :math:`\gamma_{S}^{(0,1)}(N)`
+ eko.anomalous_dimensions.as1aem1.gamma_singlet : :math:`\gamma_{S}^{(1,1)}(N)`
+ eko.anomalous_dimensions.aem2.gamma_singlet : :math:`\gamma_{S}^{(0,2)}(N)`
+ """
+ # cache the s-es
+ max_weight = max(order)
+ if max_weight >= 3:
+ # here we need only S1,S2,S3,S4
+ sx = harmonics.sx(n, max_weight=max_weight + 1)
+ else:
+ sx = harmonics.sx(n, max_weight=3)
+ sx_ns_qed = harmonics.compute_qed_ns_cache(n, sx[0])
+ gamma_s = np.zeros((order[0] + 1, order[1] + 1, 4, 4), np.complex_)
+ gamma_s[1, 0] = as1.gamma_singlet_qed(n, sx[0], nf)
+ gamma_s[0, 1] = aem1.gamma_singlet(n, nf, sx)
+ gamma_s[1, 1] = as1aem1.gamma_singlet(n, nf, sx, sx_ns_qed)
+ if order[0] >= 2:
+ gamma_s[2, 0] = as2.gamma_singlet_qed(n, nf, sx)
+ if order[1] >= 2:
+ gamma_s[0, 2] = aem2.gamma_singlet(n, nf, sx, sx_ns_qed)
+ if order[0] >= 3:
+ gamma_s[3, 0] = as3.gamma_singlet_qed(n, nf, sx)
+ return gamma_s
+
+
+@nb.njit(cache=True)
+def gamma_valence_qed(order, n, nf):
+ r"""
+ Compute the grid of the QED valence anomalous dimensions matrices.
+
+ Parameters
+ ----------
+ order : tuple(int,int)
+ perturbative orders
+ n : complex
+ Mellin variable
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_valence : numpy.ndarray
+ valence anomalous dimensions matrices
+
+ See Also
+ --------
+ eko.anomalous_dimensions.as1.gamma_valence_qed : :math:`\gamma_{V}^{(0)}(N)`
+ eko.anomalous_dimensions.as2.gamma_valence_qed : :math:`\gamma_{V}^{(1)}(N)`
+ eko.anomalous_dimensions.as3.gamma_valence_qed : :math:`\gamma_{V}^{(2)}(N)`
+ eko.anomalous_dimensions.aem1.gamma_valence : :math:`\gamma_{V}^{(0,1)}(N)`
+ eko.anomalous_dimensions.as1aem1.gamma_valence : :math:`\gamma_{V}^{(1,1)}(N)`
+ eko.anomalous_dimensions.aem2.gamma_valence : :math:`\gamma_{V}^{(0,2)}(N)`
+ """
+ # cache the s-es
+ max_weight = max(order)
+ if max_weight >= 3:
+ # here we need only S1,S2,S3,S4
+ sx = harmonics.sx(n, max_weight=max_weight + 1)
+ else:
+ sx = harmonics.sx(n, max_weight=3)
+ sx_ns_qed = harmonics.compute_qed_ns_cache(n, sx[0])
+ gamma_v = np.zeros((order[0] + 1, order[1] + 1, 2, 2), np.complex_)
+ gamma_v[1, 0] = as1.gamma_valence_qed(n, sx[0])
+ gamma_v[0, 1] = aem1.gamma_valence(n, nf, sx)
+ gamma_v[1, 1] = as1aem1.gamma_valence(n, nf, sx, sx_ns_qed)
+ if order[0] >= 2:
+ gamma_v[2, 0] = as2.gamma_valence_qed(n, nf, sx)
+ if order[1] >= 2:
+ gamma_v[0, 2] = aem2.gamma_valence(n, nf, sx, sx_ns_qed)
+ if order[0] >= 3:
+ gamma_v[3, 0] = as3.gamma_valence_qed(n, nf, sx)
+ return gamma_v
diff --git a/src/ekore/anomalous_dimensions/unpolarized/space_like/aem1.py b/src/ekore/anomalous_dimensions/unpolarized/space_like/aem1.py
index d26683969..7f122938a 100644
--- a/src/ekore/anomalous_dimensions/unpolarized/space_like/aem1.py
+++ b/src/ekore/anomalous_dimensions/unpolarized/space_like/aem1.py
@@ -1,38 +1,35 @@
-"""
-This file contains the O(aem1) Altarelli-Parisi splitting kernels.
-"""
+"""The :math:`O(a_{em}^1)` Altarelli-Parisi splitting kernels."""
import numba as nb
+import numpy as np
from eko import constants
+
from . import as1
@nb.njit(cache=True)
def gamma_phq(N):
- """
- Computes the leading-order photon-quark anomalous dimension
+ r"""Compute the leading-order photon-quark anomalous dimension.
Implements Eq. (2.5) of :cite:`Carrazza:2015dea`.
Parameters
----------
- N : complex
+ N : complex
Mellin moment
Returns
-------
- gamma_phq : complex
- Leading-order photon-quark anomalous dimension :math:`\\gamma_{\\gamma q}^{(0)}(N)`
+ gamma_phq : complex
+ Leading-order photon-quark anomalous dimension :math:`\\gamma_{\\gamma q}^{(0,1)}(N)`
"""
-
return as1.gamma_gq(N) / constants.CF
@nb.njit(cache=True)
def gamma_qph(N, nf):
- """
- Computes the leading-order quark-photon anomalous dimension
+ r"""Compute the leading-order quark-photon anomalous dimension.
Implements Eq. (2.5) of :cite:`Carrazza:2015dea`.
But adding the :math:`N_C` and the :math:`2n_f` factors from :math:`\\theta` inside the
@@ -40,57 +37,161 @@ def gamma_qph(N, nf):
Parameters
----------
- N : complex
+ N : complex
Mellin moment
- nf : int
+ nf : int
Number of active flavors
Returns
-------
- gamma_qph : complex
- Leading-order quark-photon anomalous dimension :math:`\\gamma_{q \\gamma}^{(0)}(N)`
+ gamma_qph : complex
+ Leading-order quark-photon anomalous dimension :math:`\\gamma_{q \\gamma}^{(0,1)}(N)`
"""
return as1.gamma_qg(N, nf) / constants.TR * constants.NC
@nb.njit(cache=True)
def gamma_phph(nf):
- """
- Computes the leading-order photon-photon anomalous dimension
+ r"""Compute the leading-order photon-photon anomalous dimension.
Implements Eq. (2.5) of :cite:`Carrazza:2015dea`.
Parameters
----------
- nf : int
+ nf : int
Number of active flavors
Returns
-------
- gamma_phph : complex
- Leading-order phton-photon anomalous dimension :math:`\\gamma_{\\gamma \\gamma}^{(0)}(N)`
+ gamma_phph : complex
+ Leading-order phton-photon anomalous dimension :math:`\\gamma_{\\gamma \\gamma}^{(0,1)}(N)`
"""
-
- return 2 / 3 * constants.NC * 2 * nf
+ nu = constants.uplike_flavors(nf)
+ nd = nf - nu
+ return 4.0 / 3.0 * constants.NC * (nu * constants.eu2 + nd * constants.ed2)
@nb.njit(cache=True)
-def gamma_ns(N, s1):
- """
- Computes the leading-order non-singlet QED anomalous dimension.
+def gamma_ns(N, sx):
+ r"""Compute the leading-order non-singlet QED anomalous dimension.
Implements Eq. (2.5) of :cite:`Carrazza:2015dea`.
Parameters
----------
- N : complex
+ N : complex
Mellin moment
- s1 : complex
+ s1 : complex
S1(N)
Returns
-------
- gamma_ns : complex
- Leading-order non-singlet QED anomalous dimension :math:`\\gamma_{ns}^{(0)}(N)`
+ gamma_ns : complex
+ Leading-order non-singlet QED anomalous dimension :math:`\\gamma_{ns}^{(0,1)}(N)`
"""
+ s1 = sx[0]
return as1.gamma_ns(N, s1) / constants.CF
+
+
+@nb.njit(cache=True)
+def gamma_singlet(N, nf, sx):
+ r"""Compute the QED leading-order singlet anomalous dimension matrix.
+
+ .. math::
+ \\gamma_S^{(0)} = \\left(\begin{array}{cc}
+ 0 & 0 & 0 & 0 \\
+ 0 & \\gamma_{\\gamma \\gamma}^{(0,1)} & \\langle e^2 \rangle \\gamma_{\\gamma q}^{(0,1)} & \nu_u e^2_- \\gamma_{\\gamma q}^{(0,1)}\\
+ 0 & \\langle e^2 \rangle\\gamma_{q \\gamma}^{(0,1)} & \\langle e^2 \rangle \\gamma_{ns}^{(0,1)} & \nu_u e^2_- \\gamma_{ns}^{(0,1)}\\
+ 0 & \nu_d e^2_- \\gamma_{q \\gamma}^{(0,1)} & \nu_d e^2_- \\gamma_{ns}^{(0,1)} & e^2_\\Delta \\gamma_{ns}^{(0,1)}
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_S_0 : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{S}^{(0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
+ """
+ e2avg, vue2m, vde2m, e2delta = constants.charge_combinations(nf)
+ gamma_ph_q = gamma_phq(N)
+ gamma_q_ph = gamma_qph(N, nf)
+ gamma_nonsinglet = gamma_ns(N, sx)
+ gamma_S_01 = np.array(
+ [
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [
+ 0.0 + 0.0j,
+ gamma_phph(nf),
+ e2avg * gamma_ph_q,
+ vue2m * gamma_ph_q,
+ ],
+ [
+ 0.0 + 0.0j,
+ e2avg * gamma_q_ph,
+ e2avg * gamma_nonsinglet,
+ vue2m * gamma_nonsinglet,
+ ],
+ [
+ 0.0 + 0.0j,
+ vde2m * gamma_q_ph,
+ vde2m * gamma_nonsinglet,
+ e2delta * gamma_nonsinglet,
+ ],
+ ],
+ np.complex_,
+ )
+ return gamma_S_01
+
+
+@nb.njit(cache=True)
+def gamma_valence(N, nf, sx):
+ r"""Compute the QED leading-order valence anomalous dimension matrix.
+
+ .. math::
+ \\gamma_V^{(0,1)} = \\left(\begin{array}{cc}
+ \\langle e^2 \rangle \\gamma_{ns}^{(0,1)} & \nu_u e^2_- \\gamma_{ns}^{(0,1)}\\
+ \nu_d e^2_- \\gamma_{ns}^{(0,1)} & e^2_\\Delta \\gamma_{ns}^{(0,1)}
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+
+ Returns
+ -------
+ gamma_S_0 : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{S}^{(0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
+ """
+ e2avg, vue2m, vde2m, e2delta = constants.charge_combinations(nf)
+ gamma_V_01 = np.array(
+ [
+ [e2avg, vue2m],
+ [vde2m, e2delta],
+ ],
+ np.complex_,
+ )
+ return gamma_V_01 * gamma_ns(N, sx)
diff --git a/src/ekore/anomalous_dimensions/unpolarized/space_like/aem2.py b/src/ekore/anomalous_dimensions/unpolarized/space_like/aem2.py
index 83f40e456..3a3eef329 100644
--- a/src/ekore/anomalous_dimensions/unpolarized/space_like/aem2.py
+++ b/src/ekore/anomalous_dimensions/unpolarized/space_like/aem2.py
@@ -1,30 +1,33 @@
-"""
-This file contains the O(aem2) Altarelli-Parisi splitting kernels.
-"""
+"""The :math:`O(a_{em}^2)` Altarelli-Parisi splitting kernels."""
import numba as nb
+import numpy as np
from eko import constants
+
from . import as1aem1
@nb.njit(cache=True)
def gamma_phph(N, nf):
- """Computes the O(aem2) photon-photon singlet anomalous dimension.
+ r"""Compute the :math:`O(a_{em}^2)` photon-photon singlet anomalous dimension.
Implements Eq. (68) of :cite:`deFlorian:2016gvk`.
Parameters
----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
Returns
-------
- gamma_gg : complex
- O(aem2) photon-photon singlet anomalous dimension
- :math:`\\gamma_{\\gamma \\gamma}^{(0,2)}(N)`
+ gamma_gg : complex
+ :math:`O(a_{em}^2)` photon-photon singlet anomalous dimension
+ :math:`\\gamma_{\\gamma \\gamma}^{(0,2)}(N)`
"""
-
nu = constants.uplike_flavors(nf)
nd = nf - nu
return (
@@ -36,23 +39,23 @@ def gamma_phph(N, nf):
@nb.njit(cache=True)
def gamma_uph(N, nf, sx):
- """Computes the O(aem2) quark-photon anomalous dimension for up quarks.
+ r"""Compute the :math:`O(a_{em}^2)` quark-photon anomalous dimension for up quarks.
Implements Eq. (55) of :cite:`deFlorian:2016gvk` for q=u.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_uph : complex
- O(aem2) quark-photon anomalous dimension :math:`\\gamma_{u \\gamma}^{(0,2)}(N)`
+ gamma_uph : complex
+ :math:`O(a_{em}^2)` quark-photon anomalous dimension :math:`\\gamma_{u \\gamma}^{(0,2)}(N)`
"""
return constants.eu2 * as1aem1.gamma_qph(N, nf, sx) / constants.CF
@@ -60,23 +63,23 @@ def gamma_uph(N, nf, sx):
@nb.njit(cache=True)
def gamma_dph(N, nf, sx):
- """Computes the O(aem2) quark-photon anomalous dimension for down quarks.
+ r"""Compute the :math:`O(a_{em}^2)` quark-photon anomalous dimension for down quarks.
Implements Eq. (55) of :cite:`deFlorian:2016gvk` for q=d.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_dph : complex
- O(aem2) quark-photon anomalous dimension :math:`\\gamma_{d \\gamma}^{(0,2)}(N)`
+ gamma_dph : complex
+ :math:`O(a_{em}^2)` quark-photon anomalous dimension :math:`\\gamma_{d \\gamma}^{(0,2)}(N)`
"""
return constants.ed2 * as1aem1.gamma_qph(N, nf, sx) / constants.CF
@@ -84,32 +87,32 @@ def gamma_dph(N, nf, sx):
@nb.njit(cache=True)
def gamma_phu(N, nf, sx):
- """Computes the O(aem2) photon-quark anomalous dimension for up quarks.
+ r"""Compute the :math:`O(a_{em}^2)` photon-quark anomalous dimension for up quarks.
Implements Eq. (56) of :cite:`deFlorian:2016gvk` for q=u.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_phu : complex
- O(aem2) photon-quark anomalous dimension :math:`\\gamma_{\\gamma u}^{(0,2)}(N)`
+ gamma_phu : complex
+ :math:`O(a_{em}^2)` photon-quark anomalous dimension :math:`\\gamma_{\\gamma u}^{(0,2)}(N)`
"""
nu = constants.uplike_flavors(nf)
nd = nf - nu
S1 = sx[0]
- tmp = (-16 * (-16 - 27 * N - 13 * N**2 - 8 * N**3)) / (
- 9.0 * (-1 + N) * N * (1 + N) ** 2
- ) - 16 * (2 + 3 * N + 2 * N**2 + N**3) / (
- 3.0 * (-1 + N) * N * (1 + N) ** 2
+ tmp = (-16.0 * (-16.0 - 27.0 * N - 13.0 * N**2 - 8.0 * N**3)) / (
+ 9.0 * (-1.0 + N) * N * (1.0 + N) ** 2
+ ) - 16.0 * (2.0 + 3.0 * N + 2.0 * N**2 + N**3) / (
+ 3.0 * (-1.0 + N) * N * (1.0 + N) ** 2
) * S1
eSigma2 = constants.NC * (nu * constants.eu2 + nd * constants.ed2)
return constants.eu2 * as1aem1.gamma_phq(N, sx) / constants.CF + eSigma2 * tmp
@@ -117,57 +120,57 @@ def gamma_phu(N, nf, sx):
@nb.njit(cache=True)
def gamma_phd(N, nf, sx):
- """Computes the O(aem2) photon-quark anomalous dimension for down quarks.
+ r"""Compute the :math:`O(a_{em}^2)` photon-quark anomalous dimension for down quarks.
Implements Eq. (56) of :cite:`deFlorian:2016gvk` for q=d.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_phd : complex
- O(aem2) photon-quark anomalous dimension :math:`\\gamma_{\\gamma d}^{(0,2)}(N)`
+ gamma_phd : complex
+ :math:`O(a_{em}^2)` photon-quark anomalous dimension :math:`\\gamma_{\\gamma d}^{(0,2)}(N)`
"""
nu = constants.uplike_flavors(nf)
nd = nf - nu
S1 = sx[0]
- tmp = (-16 * (-16 - 27 * N - 13 * N**2 - 8 * N**3)) / (
- 9.0 * (-1 + N) * N * (1 + N) ** 2
- ) - 16 * (2 + 3 * N + 2 * N**2 + N**3) / (
- 3.0 * (-1 + N) * N * (1 + N) ** 2
+ tmp = (-16.0 * (-16.0 - 27.0 * N - 13.0 * N**2 - 8.0 * N**3)) / (
+ 9.0 * (-1.0 + N) * N * (1.0 + N) ** 2
+ ) - 16.0 * (2.0 + 3.0 * N + 2.0 * N**2 + N**3) / (
+ 3.0 * (-1.0 + N) * N * (1.0 + N) ** 2
) * S1
eSigma2 = constants.NC * (nu * constants.eu2 + nd * constants.ed2)
return constants.ed2 * as1aem1.gamma_phq(N, sx) / constants.CF + eSigma2 * tmp
@nb.njit(cache=True)
-def gamma_nspu(N, nf, sx):
- """Computes the O(aem2) singlet-like non-singlet anomalous dimension for up quarks.
+def gamma_nspu(N, nf, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_{em}^2)` singlet-like non-singlet anomalous dimension for up quarks.
Implements sum of Eqs. (57-58) of :cite:`deFlorian:2016gvk` for q=u.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_nspu : complex
- O(aem2) singlet-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,+,u}^{(0,2)}(N)`
+ gamma_nspu : complex
+ :math:`O(a_{em}^2)` singlet-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,+,u}^{(0,2)}(N)`
"""
S1 = sx[0]
@@ -176,35 +179,37 @@ def gamma_nspu(N, nf, sx):
nd = nf - nu
eSigma2 = constants.NC * (nu * constants.eu2 + nd * constants.ed2)
tmp = (
- 2
- * (-12 + 20 * N + 47 * N**2 + 6 * N**3 + 3 * N**4)
- / (9.0 * N**2 * (1 + N) ** 2)
- - 80 / 9 * S1
- + 16 / 3 * S2
+ 2.0
+ * (-12.0 + 20.0 * N + 47.0 * N**2 + 6.0 * N**3 + 3.0 * N**4)
+ / (9.0 * N**2 * (1.0 + N) ** 2)
+ - 80.0 / 9.0 * S1
+ + 16.0 / 3.0 * S2
) * eSigma2
- return constants.eu2 * as1aem1.gamma_nsp(N, sx) / constants.CF / 2 + tmp
+ return (
+ constants.eu2 * as1aem1.gamma_nsp(N, sx, sx_ns_qed) / constants.CF / 2.0 + tmp
+ )
@nb.njit(cache=True)
-def gamma_nspd(N, nf, sx):
- """Computes the O(aem2) singlet-like non-singlet anomalous dimension for down quarks.
+def gamma_nspd(N, nf, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_{em}^2)` singlet-like non-singlet anomalous dimension for down quarks.
Implements sum of Eqs. (57-58) of :cite:`deFlorian:2016gvk` for q=d.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_nspd : complex
- O(aem2) singlet-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,+,d}^{(0,2)}(N)`
+ gamma_nspd : complex
+ :math:`O(a_{em}^2)` singlet-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,+,d}^{(0,2)}(N)`
"""
S1 = sx[0]
@@ -213,35 +218,37 @@ def gamma_nspd(N, nf, sx):
nd = nf - nu
eSigma2 = constants.NC * (nu * constants.eu2 + nd * constants.ed2)
tmp = (
- 2
- * (-12 + 20 * N + 47 * N**2 + 6 * N**3 + 3 * N**4)
- / (9.0 * N**2 * (1 + N) ** 2)
- - 80 / 9 * S1
- + 16 / 3 * S2
+ 2.0
+ * (-12.0 + 20.0 * N + 47.0 * N**2 + 6.0 * N**3 + 3.0 * N**4)
+ / (9.0 * N**2 * (1.0 + N) ** 2)
+ - 80.0 / 9.0 * S1
+ + 16.0 / 3.0 * S2
) * eSigma2
- return constants.ed2 * as1aem1.gamma_nsp(N, sx) / constants.CF / 2 + tmp
+ return (
+ constants.ed2 * as1aem1.gamma_nsp(N, sx, sx_ns_qed) / constants.CF / 2.0 + tmp
+ )
@nb.njit(cache=True)
-def gamma_nsmu(N, nf, sx):
- """Computes the O(aem2) valence-like non-singlet anomalous dimension for up quarks.
+def gamma_nsmu(N, nf, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_{em}^2)` valence-like non-singlet anomalous dimension for up quarks.
Implements difference between Eqs. (57-58) of :cite:`deFlorian:2016gvk` for q=u.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_nsp : complex
- O(aem2) valence-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,-,u}^{(0,2)}(N)`
+ gamma_nsp : complex
+ :math:`O(a_{em}^2)` valence-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,-,u}^{(0,2)}(N)`
"""
S1 = sx[0]
@@ -251,34 +258,36 @@ def gamma_nsmu(N, nf, sx):
eSigma2 = constants.NC * (nu * constants.eu2 + nd * constants.ed2)
tmp = (
2
- * (-12 + 20 * N + 47 * N**2 + 6 * N**3 + 3 * N**4)
- / (9.0 * N**2 * (1 + N) ** 2)
- - 80 / 9 * S1
- + 16 / 3 * S2
+ * (-12.0 + 20.0 * N + 47.0 * N**2 + 6.0 * N**3 + 3.0 * N**4)
+ / (9.0 * N**2 * (1.0 + N) ** 2)
+ - 80.0 / 9.0 * S1
+ + 16.0 / 3.0 * S2
) * eSigma2
- return constants.eu2 * as1aem1.gamma_nsm(N, sx) / constants.CF / 2 + tmp
+ return (
+ constants.eu2 * as1aem1.gamma_nsm(N, sx, sx_ns_qed) / constants.CF / 2.0 + tmp
+ )
@nb.njit(cache=True)
-def gamma_nsmd(N, nf, sx):
- """Computes the O(aem2) valence-like non-singlet anomalous dimension for down quarks.
+def gamma_nsmd(N, nf, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_{em}^2)` valence-like non-singlet anomalous dimension for down quarks.
Implements difference between Eqs. (57-58) of :cite:`deFlorian:2016gvk` for q=d.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_nsp : complex
- O(aem2) valence-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,-,d}^{(0,2)}(N)`
+ gamma_nsp : complex
+ :math:`O(a_{em}^2)` valence-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,-,d}^{(0,2)}(N)`
"""
S1 = sx[0]
@@ -287,39 +296,154 @@ def gamma_nsmd(N, nf, sx):
nd = nf - nu
eSigma2 = constants.NC * (nu * constants.eu2 + nd * constants.ed2)
tmp = (
- 2
- * (-12 + 20 * N + 47 * N**2 + 6 * N**3 + 3 * N**4)
- / (9.0 * N**2 * (1 + N) ** 2)
- - 80 / 9 * S1
- + 16 / 3 * S2
+ 2.0
+ * (-12.0 + 20.0 * N + 47.0 * N**2 + 6.0 * N**3 + 3.0 * N**4)
+ / (9.0 * N**2 * (1.0 + N) ** 2)
+ - 80.0 / 9.0 * S1
+ + 16.0 / 3.0 * S2
) * eSigma2
- return constants.ed2 * as1aem1.gamma_nsm(N, sx) / constants.CF / 2 + tmp
+ return (
+ constants.ed2 * as1aem1.gamma_nsm(N, sx, sx_ns_qed) / constants.CF / 2.0 + tmp
+ )
@nb.njit(cache=True)
def gamma_ps(N, nf):
- """Computes the O(aem2) pure-singlet quark-quark anomalous dimension.
+ r"""Compute the :math:`O(a_{em}^2)` pure-singlet quark-quark anomalous dimension.
Implements Eq. (59) of :cite:`deFlorian:2016gvk`.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
Returns
-------
- gamma_ps : complex
- O(aem2) pure-singlet quark-quark anomalous dimension
- :math:`\\gamma_{ps}^{(0,2)}(N)`
+ gamma_ps : complex
+ :math:`O(a_{em}^2)` pure-singlet quark-quark anomalous dimension
+ :math:`\\gamma_{ps}^{(0,2)}(N)`
"""
result = (
- -4
- * (2 + N * (5 + N))
- * (4 + N * (4 + N * (7 + 5 * N)))
- / ((-1 + N) * N**3 * (1 + N) ** 3 * (2 + N) ** 2)
+ -4.0
+ * (2.0 + N * (5.0 + N))
+ * (4.0 + N * (4.0 + N * (7.0 + 5.0 * N)))
+ / ((-1.0 + N) * N**3 * (1.0 + N) ** 3 * (2.0 + N) ** 2)
)
return 2 * nf * constants.CA * result
+
+
+@nb.njit(cache=True)
+def gamma_singlet(N, nf, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_{em}^2)` singlet sector.
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
+
+ Returns
+ -------
+ gamma_singlet : numpy.ndarray
+ :math:`O(a_{em}^2)` singlet anomalous dimension :math:`\\gamma_{S}^{(0,2)}(N,nf,sx)`
+ """
+ nu = constants.uplike_flavors(nf)
+ nd = nf - nu
+ vu = nu / nf
+ vd = nd / nf
+ e2m = constants.eu2 - constants.ed2
+ e2avg = (nu * constants.eu2 + nd * constants.ed2) / nf
+ e2m = constants.eu2 - constants.ed2
+ gamma_ph_u = gamma_phu(N, nf, sx)
+ gamma_ph_d = gamma_phd(N, nf, sx)
+ gamma_u_ph = gamma_uph(N, nf, sx)
+ gamma_d_ph = gamma_dph(N, nf, sx)
+ gamma_ns_p_u = gamma_nspu(N, nf, sx, sx_ns_qed)
+ gamma_ns_p_d = gamma_nspd(N, nf, sx, sx_ns_qed)
+ gamma_pure_singlet = gamma_ps(N, nf)
+ gamma_S_02 = np.array(
+ [
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [
+ 0.0 + 0.0j,
+ gamma_phph(N, nf),
+ vu * constants.eu2 * gamma_ph_u + vd * constants.ed2 * gamma_ph_d,
+ vu * (constants.eu2 * gamma_ph_u - constants.ed2 * gamma_ph_d),
+ ],
+ [
+ 0.0 + 0.0j,
+ vu * constants.eu2 * gamma_u_ph + vd * constants.ed2 * gamma_d_ph,
+ vu * constants.eu2 * gamma_ns_p_u
+ + vd * constants.ed2 * gamma_ns_p_d
+ + e2avg**2 * gamma_pure_singlet,
+ vu
+ * (
+ constants.eu2 * gamma_ns_p_u
+ - constants.ed2 * gamma_ns_p_d
+ + e2m * e2avg * gamma_pure_singlet
+ ),
+ ],
+ [
+ 0.0 + 0.0j,
+ vd * (constants.eu2 * gamma_u_ph - constants.ed2 * gamma_d_ph),
+ vd
+ * (
+ constants.eu2 * gamma_ns_p_u
+ - constants.ed2 * gamma_ns_p_d
+ + e2m * e2avg * gamma_pure_singlet
+ ),
+ vd * constants.eu2 * gamma_ns_p_u
+ + vu * constants.ed2 * gamma_ns_p_d
+ + vu * vd * e2m**2 * gamma_pure_singlet,
+ ],
+ ],
+ np.complex_,
+ )
+ return gamma_S_02
+
+
+@nb.njit(cache=True)
+def gamma_valence(N, nf, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_{em}^2)` valence sector.
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
+
+ Returns
+ -------
+ gamma_singlet : numpy.ndarray
+ :math:`O(a_{em}^2)` valence anomalous dimension :math:`\\gamma_{V}^{(0,2)}(N,nf,sx)`
+ """
+ nu = constants.uplike_flavors(nf)
+ nd = nf - nu
+ vu = nu / nf
+ vd = nd / nf
+ gamma_ns_m_u = gamma_nsmu(N, nf, sx, sx_ns_qed)
+ gamma_ns_m_d = gamma_nsmd(N, nf, sx, sx_ns_qed)
+ gamma_V_02 = np.array(
+ [
+ [
+ vu * constants.eu2 * gamma_ns_m_u + vd * constants.ed2 * gamma_ns_m_d,
+ vu * (constants.eu2 * gamma_ns_m_u - constants.ed2 * gamma_ns_m_d),
+ ],
+ [
+ vd * (constants.eu2 * gamma_ns_m_u - constants.ed2 * gamma_ns_m_d),
+ vd * constants.eu2 * gamma_ns_m_u + vu * constants.ed2 * gamma_ns_m_d,
+ ],
+ ],
+ np.complex_,
+ )
+ return gamma_V_02
diff --git a/src/ekore/anomalous_dimensions/unpolarized/space_like/as1.py b/src/ekore/anomalous_dimensions/unpolarized/space_like/as1.py
index 28afb3a00..4a6ab875e 100644
--- a/src/ekore/anomalous_dimensions/unpolarized/space_like/as1.py
+++ b/src/ekore/anomalous_dimensions/unpolarized/space_like/as1.py
@@ -1,4 +1,4 @@
-"""This file contains the leading-order Altarelli-Parisi splitting kernels."""
+"""Compute the leading-order Altarelli-Parisi splitting kernels."""
import numba as nb
import numpy as np
@@ -8,21 +8,20 @@
@nb.njit(cache=True)
def gamma_ns(N, s1):
- """
- Computes the leading-order non-singlet anomalous dimension.
+ r"""Compute the leading-order non-singlet anomalous dimension.
Implements Eq. (3.4) of :cite:`Moch:2004pa`.
Parameters
----------
- N : complex
+ N : complex
Mellin moment
- s1 : complex
+ s1 : complex
harmonic sum :math:`S_{1}`
Returns
-------
- gamma_ns : complex
+ gamma_ns : complex
Leading-order non-singlet anomalous dimension :math:`\\gamma_{ns}^{(0)}(N)`
"""
gamma = -(3.0 - 4.0 * s1 + 2.0 / N / (N + 1.0))
@@ -32,21 +31,20 @@ def gamma_ns(N, s1):
@nb.njit(cache=True)
def gamma_qg(N, nf):
- """
- Computes the leading-order quark-gluon anomalous dimension
+ r"""Compute the leading-order quark-gluon anomalous dimension.
Implements Eq. (3.5) of :cite:`Vogt:2004mw`.
Parameters
----------
- N : complex
+ N : complex
Mellin moment
- nf : int
+ nf : int
Number of active flavors
Returns
-------
- gamma_qg : complex
+ gamma_qg : complex
Leading-order quark-gluon anomalous dimension :math:`\\gamma_{qg}^{(0)}(N)`
"""
gamma = -(N**2 + N + 2.0) / (N * (N + 1.0) * (N + 2.0))
@@ -56,19 +54,18 @@ def gamma_qg(N, nf):
@nb.njit(cache=True)
def gamma_gq(N):
- """
- Computes the leading-order gluon-quark anomalous dimension
+ r"""Compute the leading-order gluon-quark anomalous dimension.
Implements Eq. (3.5) of :cite:`Vogt:2004mw`.
Parameters
----------
- N : complex
+ N : complex
Mellin moment
Returns
-------
- gamma_gq : complex
+ gamma_gq : complex
Leading-order gluon-quark anomalous dimension :math:`\\gamma_{gq}^{(0)}(N)`
"""
gamma = -(N**2 + N + 2.0) / (N * (N + 1.0) * (N - 1.0))
@@ -78,23 +75,22 @@ def gamma_gq(N):
@nb.njit(cache=True)
def gamma_gg(N, s1, nf):
- """
- Computes the leading-order gluon-gluon anomalous dimension
+ r"""Compute the leading-order gluon-gluon anomalous dimension.
Implements Eq. (3.5) of :cite:`Vogt:2004mw`.
Parameters
----------
- N : complex
+ N : complex
Mellin moment
- s1 : complex
+ s1 : complex
harmonic sum :math:`S_{1}`
- nf : int
+ nf : int
Number of active flavors
Returns
-------
- gamma_gg : complex
+ gamma_gg : complex
Leading-order gluon-gluon anomalous dimension :math:`\\gamma_{gg}^{(0)}(N)`
"""
gamma = s1 - 1.0 / N / (N - 1.0) - 1.0 / (N + 1.0) / (N + 2.0)
@@ -104,38 +100,122 @@ def gamma_gg(N, s1, nf):
@nb.njit(cache=True)
def gamma_singlet(N, s1, nf):
- r"""
- Computes the leading-order singlet anomalous dimension matrix
-
- .. math::
- \gamma_S^{(0)} = \left(\begin{array}{cc}
- \gamma_{qq}^{(0)} & \gamma_{qg}^{(0)}\\
- \gamma_{gq}^{(0)} & \gamma_{gg}^{(0)}
- \end{array}\right)
-
- Parameters
- ----------
- N : complex
- Mellin moment
- s1 : complex
- harmonic sum :math:`S_{1}`
- nf : int
- Number of active flavors
-
- Returns
- -------
- gamma_S_0 : numpy.ndarray
- Leading-order singlet anomalous dimension matrix :math:`\gamma_{S}^{(0)}(N)`
-
- See Also
- --------
- gamma_ns : :math:`\gamma_{qq}^{(0)}`
- gamma_qg : :math:`\gamma_{qg}^{(0)}`
- gamma_gq : :math:`\gamma_{gq}^{(0)}`
- gamma_gg : :math:`\gamma_{gg}^{(0)}`
+ r"""Compute the leading-order singlet anomalous dimension matrix.
+
+ .. math::
+ \\gamma_S^{(0)} = \\left(\begin{array}{cc}
+ \\gamma_{qq}^{(0)} & \\gamma_{qg}^{(0)}\\
+ \\gamma_{gq}^{(0)} & \\gamma_{gg}^{(0)}
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_S_0 : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{S}^{(0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
"""
gamma_qq = gamma_ns(N, s1)
gamma_S_0 = np.array(
[[gamma_qq, gamma_qg(N, nf)], [gamma_gq(N), gamma_gg(N, s1, nf)]], np.complex_
)
return gamma_S_0
+
+
+@nb.njit(cache=True)
+def gamma_singlet_qed(N, s1, nf):
+ r"""Compute the leading-order singlet anomalous dimension matrix for the unified evolution basis.
+
+ .. math::
+ \\gamma_S^{(1,0)} = \\left(\begin{array}{cccc}
+ \\gamma_{gg}^{(1,0)} & 0 & \\gamma_{gq}^{(1,0)} & 0\\
+ 0 & 0 & 0 & 0 \\
+ \\gamma_{qg}^{(1,0)} & 0 & \\gamma_{qq}^{(1,0)} & 0 \\
+ 0 & 0 & 0 & \\gamma_{qq}^{(1,0)} \\
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_S : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{S}^{(1,0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
+ """
+ gamma_qq = gamma_ns(N, s1)
+ gamma_S = np.array(
+ [
+ [gamma_gg(N, s1, nf), 0.0 + 0.0j, gamma_gq(N), 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [gamma_qg(N, nf), 0.0 + 0.0j, gamma_qq, 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, gamma_qq],
+ ],
+ np.complex_,
+ )
+ return gamma_S
+
+
+@nb.njit(cache=True)
+def gamma_valence_qed(N, s1):
+ r"""Compute the leading-order valence anomalous dimension matrix for the unified evolution basis.
+
+ .. math::
+ \\gamma_V^{(1,0)} = \\left(\begin{array}{cc}
+ \\gamma_{ns}^{(1,0)} & 0\\
+ 0 & \\gamma_{ns}^{(1,0)}
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+
+ Returns
+ -------
+ gamma_V : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{V}^{(1,0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
+ """
+ gamma_V = np.array(
+ [
+ [1.0, 0.0],
+ [0.0, 1.0],
+ ],
+ np.complex_,
+ )
+ return gamma_V * gamma_ns(N, s1)
diff --git a/src/ekore/anomalous_dimensions/unpolarized/space_like/as1aem1.py b/src/ekore/anomalous_dimensions/unpolarized/space_like/as1aem1.py
index e16133a05..fa716d94d 100644
--- a/src/ekore/anomalous_dimensions/unpolarized/space_like/as1aem1.py
+++ b/src/ekore/anomalous_dimensions/unpolarized/space_like/as1aem1.py
@@ -1,92 +1,100 @@
-"""
-This file contains the O(as1aem1) Altarelli-Parisi splitting kernels.
-"""
+"""The :math:`O(a_s^1a_{em}^1)` Altarelli-Parisi splitting kernels."""
import numba as nb
+import numpy as np
from eko import constants
+
from .... import harmonics
from ....harmonics.constants import zeta2, zeta3
@nb.njit(cache=True)
def gamma_phq(N, sx):
- """Computes the O(as1aem1) photon-quark anomalous dimension
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` photon-quark anomalous dimension.
Implements Eq. (36) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- N : complex
- Mellin moment
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_phq : complex
- O(as1aem1) photon-quark anomalous dimension :math:`\\gamma_{\\gamma q}^{(1,1)}(N)`
+ gamma_phq : complex
+ :math:`O(a_s^1a_{em}^1)` photon-quark anomalous dimension :math:`\\gamma_{\\gamma q}^{(1,1)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
tmp_const = (
- 2
- * (-4 - 12 * N - N**2 + 28 * N**3 + 43 * N**4 + 30 * N**5 + 12 * N**6)
- / ((-1 + N) * N**3 * (1 + N) ** 3)
+ 2.0
+ * (
+ -4.0
+ - 12.0 * N
+ - N**2
+ + 28.0 * N**3
+ + 43.0 * N**4
+ + 30.0 * N**5
+ + 12.0 * N**6
+ )
+ / ((-1.0 + N) * N**3 * (1.0 + N) ** 3)
)
tmp_S1 = (
- -4
- * (10 + 27 * N + 25 * N**2 + 13 * N**3 + 5 * N**4)
- / ((-1 + N) * N * (1 + N) ** 3)
+ -4.0
+ * (10.0 + 27.0 * N + 25.0 * N**2 + 13.0 * N**3 + 5.0 * N**4)
+ / ((-1.0 + N) * N * (1.0 + N) ** 3)
)
- tmp_S12 = 4 * (2 + N + N**2) / ((-1 + N) * N * (1 + N))
- tmp_S2 = 4 * (2 + N + N**2) / ((-1 + N) * N * (1 + N))
+ tmp_S12 = 4.0 * (2.0 + N + N**2) / ((-1.0 + N) * N * (1.0 + N))
+ tmp_S2 = 4.0 * (2.0 + N + N**2) / ((-1.0 + N) * N * (1.0 + N))
return constants.CF * (tmp_const + tmp_S1 * S1 + tmp_S12 * S1**2 + tmp_S2 * S2)
@nb.njit(cache=True)
def gamma_qph(N, nf, sx):
- """Computes the O(as1aem1) quark-photon anomalous dimension
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` quark-photon anomalous dimension.
Implements Eq. (26) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_qph : complex
- O(as1aem1) quark-photon anomalous dimension :math:`\\gamma_{q \\gamma}^{(1,1)}(N)`
+ gamma_qph : complex
+ :math:`O(a_s^1a_{em}^1)` quark-photon anomalous dimension :math:`\\gamma_{q \\gamma}^{(1,1)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
tmp_const = (
- -2
+ -2.0
* (
- 4
- + 8 * N
- + 25 * N**2
- + 51 * N**3
- + 36 * N**4
- + 15 * N**5
- + 5 * N**6
+ 4.0
+ + 8.0 * N
+ + 25.0 * N**2
+ + 51.0 * N**3
+ + 36.0 * N**4
+ + 15.0 * N**5
+ + 5.0 * N**6
)
- / (N**3 * (1 + N) ** 3 * (2 + N))
+ / (N**3 * (1.0 + N) ** 3 * (2.0 + N))
)
- tmp_S1 = 8 / N**2
- tmp_S12 = -4 * (2 + N + N**2) / (N * (1 + N) * (2 + N))
- tmp_S2 = 4 * (2 + N + N**2) / (N * (1 + N) * (2 + N))
+ tmp_S1 = 8.0 / N**2
+ tmp_S12 = -4.0 * (2.0 + N + N**2) / (N * (1.0 + N) * (2.0 + N))
+ tmp_S2 = 4.0 * (2.0 + N + N**2) / (N * (1.0 + N) * (2.0 + N))
return (
- 2
+ 2.0
* nf
* constants.CA
* constants.CF
@@ -96,92 +104,100 @@ def gamma_qph(N, nf, sx):
@nb.njit(cache=True)
def gamma_gph(N):
- """Computes the O(as1aem1) gluon-photon anomalous dimension
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` gluon-photon anomalous dimension.
Implements Eq. (27) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- N : complex
+ N : complex
Mellin moment
Returns
-------
- gamma_qph : complex
- O(as1aem1) gluon-photon anomalous dimension :math:`\\gamma_{g \\gamma}^{(1,1)}(N)`
+ gamma_qph : complex
+ :math:`O(a_s^1a_{em}^1)` gluon-photon anomalous dimension :math:`\\gamma_{g \\gamma}^{(1,1)}(N)`
"""
return (
constants.CF
* constants.CA
- * (8 * (-4 + N * (-4 + N * (-5 + N * (-10 + N + 2 * N**2 * (2 + N))))))
- / (N**3 * (1 + N) ** 3 * (-2 + N + N**2))
+ * (
+ 8.0
+ * (
+ -4.0
+ + N * (-4.0 + N * (-5.0 + N * (-10.0 + N + 2.0 * N**2 * (2.0 + N))))
+ )
+ )
+ / (N**3 * (1.0 + N) ** 3 * (-2.0 + N + N**2))
)
@nb.njit(cache=True)
def gamma_phg(N):
- """Computes the O(as1aem1) photon-gluon anomalous dimension
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` photon-gluon anomalous dimension.
Implements Eq. (30) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- N : complex
+ N : complex
Mellin moment
Returns
-------
- gamma_qph : complex
- O(as1aem1) photon-gluon anomalous dimension :math:`\\gamma_{\\gamma g}^{(1,1)}(N)`
+ gamma_qph : complex
+ :math:`O(a_s^1a_{em}^1)` photon-gluon anomalous dimension :math:`\\gamma_{\\gamma g}^{(1,1)}(N)`
"""
- return constants.TR / constants.CF / constants.CA * gamma_gph(N)
+ return constants.TR / constants.CF / constants.CA * constants.NC * gamma_gph(N)
@nb.njit(cache=True)
def gamma_qg(N, nf, sx):
- """Computes the O(as1aem1) quark-gluon singlet anomalous dimension.
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` quark-gluon singlet anomalous dimension.
Implements Eq. (29) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_qg : complex
- O(as1aem1) quark-gluon singlet anomalous dimension
- :math:`\\gamma_{qg}^{(1,1)}(N)`
+ gamma_qg : complex
+ :math:`O(a_s^1a_{em}^1)` quark-gluon singlet anomalous dimension
+ :math:`\\gamma_{qg}^{(1,1)}(N)`
"""
- return constants.TR / constants.CF / constants.CA * gamma_qph(N, nf, sx)
+ return (
+ constants.TR / constants.CF / constants.CA * constants.NC * gamma_qph(N, nf, sx)
+ )
@nb.njit(cache=True)
def gamma_gq(N, sx):
- """Computes the O(as1aem1) gluon-quark singlet anomalous dimension.
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` gluon-quark singlet anomalous dimension.
Implements Eq. (35) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- N : complex
- Mellin moment
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_gq : complex
- O(as1aem1) gluon-quark singlet anomalous dimension
- :math:`\\gamma_{gq}^{(1,1)}(N)`
+ gamma_gq : complex
+ :math:`O(a_s^1a_{em}^1)` gluon-quark singlet anomalous dimension
+ :math:`\\gamma_{gq}^{(1,1)}(N)`
"""
return gamma_phq(N, sx)
@@ -189,169 +205,254 @@ def gamma_gq(N, sx):
@nb.njit(cache=True)
def gamma_phph(nf):
- """Computes the O(as1aem1) photon-photon singlet anomalous dimension.
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` photon-photon singlet anomalous dimension.
Implements Eq. (28) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- nf : int
- Number of active flavors
+ nf : int
+ Number of active flavors
Returns
-------
- gamma_gg : complex
- O(as1aem1) photon-photon singlet anomalous dimension
- :math:`\\gamma_{\\gamma \\gamma}^{(1,1)}(N)`
+ gamma_gg : complex
+ :math:`O(a_s^1a_{em}^1)` photon-photon singlet anomalous dimension
+ :math:`\\gamma_{\\gamma \\gamma}^{(1,1)}(N)`
"""
nu = constants.uplike_flavors(nf)
nd = nf - nu
- return 4 * constants.CF * constants.CA * (nu * constants.eu2 + nd * constants.ed2)
+ return 4.0 * constants.CF * constants.CA * (nu * constants.eu2 + nd * constants.ed2)
@nb.njit(cache=True)
def gamma_gg():
- """Computes the O(as1aem1) gluon-gluon singlet anomalous dimension.
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` gluon-gluon singlet anomalous dimension.
Implements Eq. (31) of :cite:`deFlorian:2015ujt`.
- Parameters
- ----------
-
Returns
-------
- gamma_gg : complex
- O(as1aem1) gluon-gluon singlet anomalous dimension
- :math:`\\gamma_{gg}^{(1,1)}(N)`
+ gamma_gg : complex
+ :math:`O(a_s^1a_{em}^1)` gluon-gluon singlet anomalous dimension
+ :math:`\\gamma_{gg}^{(1,1)}(N)`
"""
- return 4 * constants.TR
+ return 4.0 * constants.TR * constants.NC
@nb.njit(cache=True)
-def gamma_nsp(N, sx):
- """Computes the O(as1aem1) singlet-like non-singlet anomalous dimension.
+def gamma_nsp(N, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` singlet-like non-singlet anomalous dimension.
Implements sum of Eqs. (33-34) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- N : complex
- Mellin moment
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_nsp : complex
- O(as1aem1) singlet-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,+}^{(1)}(N)`
+ gamma_nsp : complex
+ :math:`O(a_s^1a_{em}^1)` singlet-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,+}^{(1)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
S3 = sx[2]
- S1h = harmonics.S1(N / 2)
- S2h = harmonics.S2(N / 2)
- S3h = harmonics.S3(N / 2)
- S1p1h = harmonics.S1((N + 1.0) / 2)
- S2p1h = harmonics.S2((N + 1) / 2)
- S3p1h = harmonics.S3((N + 1) / 2)
- g3N = harmonics.g_functions.mellin_g3(N, S1)
- S1p2 = harmonics.polygamma.recursive_harmonic_sum(S1, N, 2, 1)
- g3Np2 = harmonics.g_functions.mellin_g3(N + 2, S1p2)
+ S1h = sx_ns_qed[0]
+ S2h = sx_ns_qed[1]
+ S3h = sx_ns_qed[2]
+ S1p1h = sx_ns_qed[3]
+ S2p1h = sx_ns_qed[4]
+ S3p1h = sx_ns_qed[5]
+ g3N = sx_ns_qed[6]
+ g3Np2 = sx_ns_qed[7]
result = (
- +32 * zeta2 * S1h
- - 32 * zeta2 * S1p1h
+ +32.0 * zeta2 * S1h
+ - 32.0 * zeta2 * S1p1h
+ 8.0 / (N + N**2) * S2h
- - 4 * S3h
- + (24 + 16 / (N + N**2)) * S2
- - 32 * S3
+ - 4.0 * S3h
+ + (24.0 + 16.0 / (N + N**2)) * S2
+ - 32.0 * S3
- 8.0 / (N + N**2) * S2p1h
+ S1
* (
- +16 * (3 / N**2 - 3 / (1 + N) ** 2 + 2 * zeta2)
- - 16 * S2h
- - 32 * S2
- + 16 * S2p1h
+ +16.0 * (3.0 / N**2 - 3.0 / (1.0 + N) ** 2 + 2.0 * zeta2)
+ - 16.0 * S2h
+ - 32.0 * S2
+ + 16.0 * S2p1h
)
+ (
- -8
+ -8.0
+ N
* (
- -32
- + N * (-8 - 3 * N * (3 + N) * (3 + N**2) - 48 * (1 + N) ** 2 * zeta2)
+ -32.0
+ + N
+ * (
+ -8.0
+ - 3.0 * N * (3.0 + N) * (3.0 + N**2)
+ - 48.0 * (1.0 + N) ** 2 * zeta2
+ )
)
)
- / (N**3 * (1 + N) ** 3)
- + 32 * (g3N + g3Np2)
- + 4 * S3p1h
- - 16 * zeta3
+ / (N**3 * (1.0 + N) ** 3)
+ + 32.0 * (g3N + g3Np2)
+ + 4.0 * S3p1h
+ - 16.0 * zeta3
)
return constants.CF * result
@nb.njit(cache=True)
-def gamma_nsm(N, sx):
- """Computes the O(as1aem1) valence-like non-singlet anomalous dimension.
+def gamma_nsm(N, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` valence-like non-singlet anomalous dimension.
Implements difference between Eqs. (33-34) of :cite:`deFlorian:2015ujt`.
Parameters
----------
- N : complex
- Mellin moment
- sx : np array
- List of harmonic sums
+ N : complex
+ Mellin moment
+ sx : np array
+ List of harmonic sums
Returns
-------
- gamma_nsm : complex
- O(as1aem1) singlet-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,-}^{(1,1)}(N)`
+ gamma_nsm : complex
+ :math:`O(a_s^1a_{em}^1)` singlet-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,-}^{(1,1)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
S3 = sx[2]
- S1h = harmonics.S1(N / 2)
- S2h = harmonics.S2(N / 2)
- S3h = harmonics.S3(N / 2)
- S1p1h = harmonics.S1((N + 1.0) / 2)
- S2p1h = harmonics.S2((N + 1) / 2)
- S3p1h = harmonics.S3((N + 1) / 2)
- g3N = harmonics.g_functions.mellin_g3(N, S1)
- S1p2 = harmonics.polygamma.recursive_harmonic_sum(S1, N, 2, 1)
- g3Np2 = harmonics.g_functions.mellin_g3(N + 2, S1p2)
-
+ S1h = sx_ns_qed[0]
+ S2h = sx_ns_qed[1]
+ S3h = sx_ns_qed[2]
+ S1p1h = sx_ns_qed[3]
+ S2p1h = sx_ns_qed[4]
+ S3p1h = sx_ns_qed[5]
+ g3N = sx_ns_qed[6]
+ g3Np2 = sx_ns_qed[7]
result = (
-32.0 * zeta2 * S1h
- 8.0 / (N + N**2) * S2h
- + (24 + 16 / (N + N**2)) * S2
+ + (24.0 + 16.0 / (N + N**2)) * S2
+ 8.0 / (N + N**2) * S2p1h
+ S1
* (
- 16 * (-1 / N**2 + 1 / (1 + N) ** 2 + 2 * zeta2)
- + 16 * S2h
- - 32 * S2
- - 16 * S2p1h
+ 16.0 * (-1.0 / N**2 + 1.0 / (1.0 + N) ** 2 + 2.0 * zeta2)
+ + 16.0 * S2h
+ - 32.0 * S2
+ - 16.0 * S2p1h
)
+ (
- 72
+ 72.0
+ N
* (
- 96
- - 3 * N * (8 + 3 * N * (3 + N) * (3 + N**2))
- + 48 * N * (1 + N) ** 2 * zeta2
+ 96.0
+ - 3.0 * N * (8.0 + 3.0 * N * (3.0 + N) * (3.0 + N**2))
+ + 48.0 * N * (1.0 + N) ** 2 * zeta2
)
)
- / (3.0 * N**3 * (1 + N) ** 3)
- - 32 * (g3N + g3Np2)
+ / (3.0 * N**3 * (1.0 + N) ** 3)
+ - 32.0 * (g3N + g3Np2)
+ 32.0 * zeta2 * S1p1h
- + 4 * S3h
- - 32 * S3
- - 4 * S3p1h
- - 16 * zeta3
+ + 4.0 * S3h
+ - 32.0 * S3
+ - 4.0 * S3p1h
+ - 16.0 * zeta3
)
return constants.CF * result
+
+
+@nb.njit(cache=True)
+def gamma_singlet(N, nf, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` singlet sector.
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
+
+ Returns
+ -------
+ gamma_singlet : numpy.ndarray
+ :math:`O(a_s^1a_{em}^1)` singlet anomalous dimension :math:`\\gamma_{S}^{(1,1)}(N,nf,sx)`
+ """
+ e2avg, vue2m, vde2m, e2delta = constants.charge_combinations(nf)
+ e2_tot = nf * e2avg
+ gamma_g_q = gamma_gq(N, sx)
+ gamma_ph_q = gamma_phq(N, sx)
+ gamma_q_g = gamma_qg(N, nf, sx)
+ gamma_q_ph = gamma_qph(N, nf, sx)
+ gamma_ns_p = gamma_nsp(N, sx, sx_ns_qed)
+ gamma_S_11 = np.array(
+ [
+ [
+ e2_tot * gamma_gg(),
+ e2_tot * gamma_gph(N),
+ e2avg * gamma_g_q,
+ vue2m * gamma_g_q,
+ ],
+ [
+ e2_tot * gamma_phg(N),
+ gamma_phph(nf),
+ e2avg * gamma_ph_q,
+ vue2m * gamma_ph_q,
+ ],
+ [
+ e2avg * gamma_q_g,
+ e2avg * gamma_q_ph,
+ e2avg * gamma_ns_p,
+ vue2m * gamma_ns_p,
+ ],
+ [
+ vde2m * gamma_q_g,
+ vde2m * gamma_q_ph,
+ vde2m * gamma_ns_p,
+ e2delta * gamma_ns_p,
+ ],
+ ],
+ np.complex_,
+ )
+ return gamma_S_11
+
+
+@nb.njit(cache=True)
+def gamma_valence(N, nf, sx, sx_ns_qed):
+ r"""Compute the :math:`O(a_s^1a_{em}^1)` valence sector.
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np array
+ List of harmonic sums
+
+ Returns
+ -------
+ gamma_singlet : numpy.ndarray
+ :math:`O(a_s^1a_{em}^1)` valence anomalous dimension :math:`\\gamma_{V}^{(1,1)}(N,nf,sx)`
+ """
+ e2avg, vue2m, vde2m, e2delta = constants.charge_combinations(nf)
+ gamma_V_11 = np.array(
+ [
+ [e2avg, vue2m],
+ [vde2m, e2delta],
+ ],
+ np.complex_,
+ )
+ return gamma_V_11 * gamma_nsm(N, sx, sx_ns_qed)
diff --git a/src/ekore/anomalous_dimensions/unpolarized/space_like/as2.py b/src/ekore/anomalous_dimensions/unpolarized/space_like/as2.py
index 8691a54b2..728c13808 100644
--- a/src/ekore/anomalous_dimensions/unpolarized/space_like/as2.py
+++ b/src/ekore/anomalous_dimensions/unpolarized/space_like/as2.py
@@ -1,5 +1,4 @@
-"""
-This file contains the |NLO| Altarelli-Parisi splitting kernels.
+"""Compute the |NLO| Altarelli-Parisi splitting kernels.
These expression have been obtained using the procedure described in the
`wiki `_
@@ -10,36 +9,38 @@
import numpy as np
from eko import constants
+
from .... import harmonics
from ....harmonics.constants import log2, zeta2, zeta3
@nb.njit(cache=True)
def gamma_nsm(n, nf, sx):
- """
- Computes the |NLO| valence-like non-singlet anomalous dimension.
+ r"""Compute the |NLO| valence-like non-singlet anomalous dimension.
Implements Eq. (3.5) of :cite:`Moch:2004pa`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : numpy.ndarray
- List of harmonic sums: :math:`S_{1},S_{2}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : numpy.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2}`
Returns
-------
- gamma_nsm : complex
- |NLO| valence-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,-}^{(1)}(N)`
+ gamma_nsm : complex
+ |NLO| valence-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,-}^{(1)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
# Here, Sp refers to S' ("s-prime") (german: "s-strich" or in Pegasus language: SSTR)
# of :cite:`Gluck:1989ze` and NOT to the Spence function a.k.a. dilogarithm
+ # TODO : these harmonic sums are computed also for the QED sector then we can use
+ # the ones that are passed to the O(as1aem1) anomalous dimensions
Sp1m = harmonics.S1((n - 1) / 2)
Sp2m = harmonics.S2((n - 1) / 2)
Sp3m = harmonics.S3((n - 1) / 2)
@@ -59,25 +60,24 @@ def gamma_nsm(n, nf, sx):
@nb.njit(cache=True)
def gamma_nsp(n, nf, sx):
- """
- Computes the |NLO| singlet-like non-singlet anomalous dimension.
+ r"""Compute the |NLO| singlet-like non-singlet anomalous dimension.
Implements Eq. (3.5) of :cite:`Moch:2004pa`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : numpy.ndarray
- List of harmonic sums: :math:`S_{1},S_{2}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : numpy.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2}`
Returns
-------
- gamma_nsp : complex
- |NLO| singlet-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,+}^{(1)}(N)`
+ gamma_nsp : complex
+ |NLO| singlet-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,+}^{(1)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -100,23 +100,22 @@ def gamma_nsp(n, nf, sx):
@nb.njit(cache=True)
def gamma_ps(n, nf):
- """
- Computes the |NLO| pure-singlet quark-quark anomalous dimension.
+ r"""Compute the |NLO| pure-singlet quark-quark anomalous dimension.
Implements Eq. (3.6) of :cite:`Vogt:2004mw`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
Returns
-------
- gamma_ps : complex
- |NLO| pure-singlet quark-quark anomalous dimension
- :math:`\\gamma_{ps}^{(1)}(N)`
+ gamma_ps : complex
+ |NLO| pure-singlet quark-quark anomalous dimension
+ :math:`\\gamma_{ps}^{(1)}(N)`
"""
# fmt: off
gqqps1_nfcf = (-4*(2 + n*(5 + n))*(4 + n*(4 + n*(7 + 5*n))))/((-1 + n)*np.power(n,3)*np.power(1 + n,3)*np.power(2 + n,2)) # pylint: disable=line-too-long
@@ -127,25 +126,24 @@ def gamma_ps(n, nf):
@nb.njit(cache=True)
def gamma_qg(n, nf, sx):
- """
- Computes the |NLO| quark-gluon singlet anomalous dimension.
+ r"""Compute the |NLO| quark-gluon singlet anomalous dimension.
Implements Eq. (3.7) of :cite:`Vogt:2004mw`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : numpy.ndarray
- List of harmonic sums: :math:`S_{1},S_{2}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : numpy.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2}`
Returns
-------
- gamma_qg : complex
- |NLO| quark-gluon singlet anomalous dimension
- :math:`\\gamma_{qg}^{(1)}(N)`
+ gamma_qg : complex
+ |NLO| quark-gluon singlet anomalous dimension
+ :math:`\\gamma_{qg}^{(1)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -162,25 +160,24 @@ def gamma_qg(n, nf, sx):
@nb.njit(cache=True)
def gamma_gq(n, nf, sx):
- """
- Computes the |NLO| gluon-quark singlet anomalous dimension.
+ r"""Compute the |NLO| gluon-quark singlet anomalous dimension.
Implements Eq. (3.8) of :cite:`Vogt:2004mw`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : numpy.ndarray
- List of harmonic sums: :math:`S_{1},S_{2}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : numpy.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2}`
Returns
-------
- gamma_gq : complex
- |NLO| gluon-quark singlet anomalous dimension
- :math:`\\gamma_{gq}^{(1)}(N)`
+ gamma_gq : complex
+ |NLO| gluon-quark singlet anomalous dimension
+ :math:`\\gamma_{gq}^{(1)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -200,25 +197,24 @@ def gamma_gq(n, nf, sx):
@nb.njit(cache=True)
def gamma_gg(n, nf, sx):
- """
- Computes the |NLO| gluon-gluon singlet anomalous dimension.
+ r"""Compute the |NLO| gluon-gluon singlet anomalous dimension.
Implements Eq. (3.9) of :cite:`Vogt:2004mw`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : numpy.ndarray
- List of harmonic sums: :math:`S_{1},S_{2}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : numpy.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2}`
Returns
-------
- gamma_gg : complex
- |NLO| gluon-gluon singlet anomalous dimension
- :math:`\\gamma_{gg}^{(1)}(N)`
+ gamma_gg : complex
+ |NLO| gluon-gluon singlet anomalous dimension
+ :math:`\\gamma_{gg}^{(1)}(N)`
"""
S1 = sx[0]
Sp1p = harmonics.S1(n / 2)
@@ -238,36 +234,35 @@ def gamma_gg(n, nf, sx):
@nb.njit(cache=True)
def gamma_singlet(n, nf, sx):
- r"""
- Computes the next-leading-order singlet anomalous dimension matrix
-
- .. math::
- \gamma_S^{(1)} = \left(\begin{array}{cc}
- \gamma_{qq}^{(1)} & \gamma_{qg}^{(1)}\\
- \gamma_{gq}^{(1)} & \gamma_{gg}^{(1)}
- \end{array}\right)
-
- Parameters
- ----------
- N : complex
- Mellin moment
- sx : numpy.ndarray
- List of harmonic sums: :math:`S_{1},S_{2}`
- nf : int
- Number of active flavors
-
- Returns
- -------
- gamma_S_1 : numpy.ndarray
- |NLO| singlet anomalous dimension matrix :math:`\gamma_{S}^{(1)}(N)`
-
- See Also
- --------
- gamma_nsp : :math:`\gamma_{qq}^{(1)}`
- gamma_ps : :math:`\gamma_{qq}^{(1)}`
- gamma_qg : :math:`\gamma_{qg}^{(1)}`
- gamma_gq : :math:`\gamma_{gq}^{(1)}`
- gamma_gg : :math:`\gamma_{gg}^{(1)}`
+ r"""Compute the next-leading-order singlet anomalous dimension matrix.
+
+ .. math::
+ \\gamma_S^{(1)} = \\left(\begin{array}{cc}
+ \\gamma_{qq}^{(1)} & \\gamma_{qg}^{(1)}\\
+ \\gamma_{gq}^{(1)} & \\gamma_{gg}^{(1)}
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ sx : numpy.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2}`
+ nf : int
+ Number of active flavors
+
+ Returns
+ -------
+ gamma_S_1 : numpy.ndarray
+ |NLO| singlet anomalous dimension matrix :math:`\\gamma_{S}^{(1)}(N)`
+
+ See Also
+ --------
+ gamma_nsp : :math:`\\gamma_{qq}^{(1)}`
+ gamma_ps : :math:`\\gamma_{qq}^{(1)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(1)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(1)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(1)}`
"""
gamma_qq = gamma_nsp(n, nf, sx) + gamma_ps(n, nf)
gamma_S_0 = np.array(
@@ -275,3 +270,91 @@ def gamma_singlet(n, nf, sx):
np.complex_,
)
return gamma_S_0
+
+
+@nb.njit(cache=True)
+def gamma_singlet_qed(N, nf, sx):
+ r"""Compute the leading-order singlet anomalous dimension matrix for the unified evolution basis.
+
+ .. math::
+ \\gamma_S^{(2,0)} = \\left(\begin{array}{cccc}
+ \\gamma_{gg}^{(2,0)} & 0 & \\gamma_{gq}^{(2,0)} & 0\\
+ 0 & 0 & 0 & 0 \\
+ \\gamma_{qg}^{(2,0)} & 0 & \\gamma_{qq}^{(2,0)} & 0 \\
+ 0 & 0 & 0 & \\gamma_{qq}^{(2,0)} \\
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+
+ Returns
+ -------
+ gamma_S : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{S}^{(2,0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
+ """
+ gamma_ns_p = gamma_nsp(N, nf, sx)
+ gamma_qq = gamma_ns_p + gamma_ps(N, nf)
+ gamma_S = np.array(
+ [
+ [gamma_gg(N, nf, sx), 0.0 + 0.0j, gamma_gq(N, nf, sx), 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [gamma_qg(N, nf, sx), 0.0 + 0.0j, gamma_qq, 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, gamma_ns_p],
+ ],
+ np.complex_,
+ )
+ return gamma_S
+
+
+@nb.njit(cache=True)
+def gamma_valence_qed(N, nf, sx):
+ r"""Compute the leading-order valence anomalous dimension matrix for the unified evolution basis.
+
+ .. math::
+ \\gamma_V^{(2,0)} = \\left(\begin{array}{cc}
+ \\gamma_{ns-}^{(2,0)} & 0\\
+ 0 & \\gamma_{ns-}^{(2,0)}
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+
+ Returns
+ -------
+ gamma_V : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{V}^{(2,0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
+ """
+ gamma_V = np.array(
+ [
+ [1.0, 0.0],
+ [0.0, 1.0],
+ ],
+ np.complex_,
+ )
+ return gamma_V * gamma_nsm(N, nf, sx)
diff --git a/src/ekore/anomalous_dimensions/unpolarized/space_like/as3.py b/src/ekore/anomalous_dimensions/unpolarized/space_like/as3.py
index a2387bd08..3bb6e05b5 100644
--- a/src/ekore/anomalous_dimensions/unpolarized/space_like/as3.py
+++ b/src/ekore/anomalous_dimensions/unpolarized/space_like/as3.py
@@ -1,5 +1,5 @@
"""
-This file contains the |NNLO| Altarelli-Parisi splitting kernels.
+Compute the |NNLO| Altarelli-Parisi splitting kernels.
The expression have been obtained from :cite:`Moch:2004pa,Vogt:2004ns`.
@@ -13,25 +13,24 @@
@nb.njit(cache=True)
def gamma_nsm(n, nf, sx):
- """
- Computes the |NNLO| valence-like non-singlet anomalous dimension.
+ r"""Compute the |NNLO| valence-like non-singlet anomalous dimension.
Implements Eq. (3.8) of :cite:`Moch:2004pa`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np.ndarray
- List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
Returns
-------
- gamma_nsm : complex
- |NNLO| valence-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,-}^{(2)}(N)`
+ gamma_nsm : complex
+ |NNLO| valence-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,-}^{(2)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -90,25 +89,24 @@ def gamma_nsm(n, nf, sx):
@nb.njit(cache=True)
def gamma_nsp(n, nf, sx):
- """
- Computes the |NNLO| singlet-like non-singlet anomalous dimension.
+ r"""Compute the |NNLO| singlet-like non-singlet anomalous dimension.
Implements Eq. (3.7) of :cite:`Moch:2004pa`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np.ndarray
- List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
Returns
-------
- gamma_nsp : complex
- |NNLO| singlet-like non-singlet anomalous dimension
- :math:`\\gamma_{ns,+}^{(2)}(N)`
+ gamma_nsp : complex
+ |NNLO| singlet-like non-singlet anomalous dimension
+ :math:`\\gamma_{ns,+}^{(2)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -167,25 +165,24 @@ def gamma_nsp(n, nf, sx):
@nb.njit(cache=True)
def gamma_nsv(n, nf, sx):
- """
- Computes the |NNLO| valence non-singlet anomalous dimension.
+ r"""Compute the |NNLO| valence non-singlet anomalous dimension.
Implements Eq. (3.9) of :cite:`Moch:2004pa`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np.ndarray
- List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
Returns
-------
- gamma_nsv : complex
- |NNLO| valence non-singlet anomalous dimension
- :math:`\\gamma_{ns,v}^{(2)}(N)`
+ gamma_nsv : complex
+ |NNLO| valence non-singlet anomalous dimension
+ :math:`\\gamma_{ns,v}^{(2)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -222,25 +219,24 @@ def gamma_nsv(n, nf, sx):
@nb.njit(cache=True)
def gamma_ps(n, nf, sx):
- """
- Computes the |NNLO| pure-singlet quark-quark anomalous dimension.
+ r"""Compute the |NNLO| pure-singlet quark-quark anomalous dimension.
Implements Eq. (3.10) of :cite:`Vogt:2004mw`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np.ndarray
- List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
Returns
-------
- gamma_ps : complex
- |NNLO| pure-singlet quark-quark anomalous dimension
- :math:`\\gamma_{ps}^{(2)}(N)`
+ gamma_ps : complex
+ |NNLO| pure-singlet quark-quark anomalous dimension
+ :math:`\\gamma_{ps}^{(2)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -294,25 +290,24 @@ def gamma_ps(n, nf, sx):
@nb.njit(cache=True)
def gamma_qg(n, nf, sx):
- """
- Computes the |NNLO| quark-gluon singlet anomalous dimension.
+ r"""Compute the |NNLO| quark-gluon singlet anomalous dimension.
Implements Eq. (3.11) of :cite:`Vogt:2004mw`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np.ndarray
- List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
Returns
-------
- gamma_qg : complex
- |NNLO| quark-gluon singlet anomalous dimension
- :math:`\\gamma_{qg}^{(2)}(N)`
+ gamma_qg : complex
+ |NNLO| quark-gluon singlet anomalous dimension
+ :math:`\\gamma_{qg}^{(2)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -368,25 +363,24 @@ def gamma_qg(n, nf, sx):
@nb.njit(cache=True)
def gamma_gq(n, nf, sx):
- """
- Computes the |NNLO| gluon-quark singlet anomalous dimension.
+ r"""Compute the |NNLO| gluon-quark singlet anomalous dimension.
Implements Eq. (3.12) of :cite:`Vogt:2004mw`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np.ndarray
- List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
Returns
-------
- gamma_gq : complex
- |NNLO| gluon-quark singlet anomalous dimension
- :math:`\\gamma_{gq}^{(2)}(N)`
+ gamma_gq : complex
+ |NNLO| gluon-quark singlet anomalous dimension
+ :math:`\\gamma_{gq}^{(2)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -458,25 +452,24 @@ def gamma_gq(n, nf, sx):
@nb.njit(cache=True)
def gamma_gg(n, nf, sx):
- """
- Computes the |NNLO| gluon-gluon singlet anomalous dimension.
+ r"""Compute the |NNLO| gluon-gluon singlet anomalous dimension.
Implements Eq. (3.13) of :cite:`Vogt:2004mw`.
Parameters
----------
- n : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np.ndarray
- List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
+ n : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
Returns
-------
- gamma_gg : complex
- |NNLO| gluon-gluon singlet anomalous dimension
- :math:`\\gamma_{gg}^{(2)}(N)`
+ gamma_gg : complex
+ |NNLO| gluon-gluon singlet anomalous dimension
+ :math:`\\gamma_{gg}^{(2)}(N)`
"""
S1 = sx[0]
S2 = sx[1]
@@ -546,38 +539,37 @@ def gamma_gg(n, nf, sx):
@nb.njit(cache=True)
def gamma_singlet(N, nf, sx):
- r"""
- Computes the |NNLO| singlet anomalous dimension matrix
-
- .. math::
- \gamma_S^{(2)} = \left(\begin{array}{cc}
- \gamma_{qq}^{(2)} & \gamma_{qg}^{(2)}\\
- \gamma_{gq}^{(2)} & \gamma_{gg}^{(2)}
- \end{array}\right)
-
- Parameters
- ----------
- N : complex
- Mellin moment
- nf : int
- Number of active flavors
- sx : np.ndarray
- List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
-
-
- Returns
- -------
- gamma_S_2 : numpy.ndarray
- |NNLO| singlet anomalous dimension matrix
- :math:`\gamma_{S}^{(2)}(N)`
-
- See Also
- --------
- gamma_nsp : :math:`\gamma_{ns,+}^{(2)}`
- gamma_ps : :math:`\gamma_{ps}^{(2)}`
- gamma_qg : :math:`\gamma_{qg}^{(2)}`
- gamma_gq : :math:`\gamma_{gq}^{(2)}`
- gamma_gg : :math:`\gamma_{gg}^{(2)}`
+ r"""Compute the |NNLO| singlet anomalous dimension matrix.
+
+ .. math::
+ \\gamma_S^{(2)} = \\left(\begin{array}{cc}
+ \\gamma_{qq}^{(2)} & \\gamma_{qg}^{(2)}\\
+ \\gamma_{gq}^{(2)} & \\gamma_{gg}^{(2)}
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ sx : np.ndarray
+ List of harmonic sums: :math:`S_{1},S_{2},S_{3}`
+
+
+ Returns
+ -------
+ gamma_S_2 : numpy.ndarray
+ |NNLO| singlet anomalous dimension matrix
+ :math:`\\gamma_{S}^{(2)}(N)`
+
+ See Also
+ --------
+ gamma_nsp : :math:`\\gamma_{ns,+}^{(2)}`
+ gamma_ps : :math:`\\gamma_{ps}^{(2)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(2)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(2)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(2)}`
"""
gamma_qq = gamma_nsp(N, nf, sx) + gamma_ps(N, nf, sx)
gamma_S_0 = np.array(
@@ -585,3 +577,91 @@ def gamma_singlet(N, nf, sx):
np.complex_,
)
return gamma_S_0
+
+
+@nb.njit(cache=True)
+def gamma_singlet_qed(N, nf, sx):
+ r"""Compute the leading-order singlet anomalous dimension matrix for the unified evolution basis.
+
+ .. math::
+ \\gamma_S^{(3,0)} = \\left(\begin{array}{cccc}
+ \\gamma_{gg}^{(3,0)} & 0 & \\gamma_{gq}^{(3,0)} & 0\\
+ 0 & 0 & 0 & 0 \\
+ \\gamma_{qg}^{(3,0)} & 0 & \\gamma_{qq}^{(3,0)} & 0 \\
+ 0 & 0 & 0 & \\gamma_{qq}^{(3,0)} \\
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+
+ Returns
+ -------
+ gamma_S : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{S}^{(3,0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
+ """
+ gamma_np_p = gamma_nsp(N, nf, sx)
+ gamma_qq = gamma_np_p + gamma_ps(N, nf, sx)
+ gamma_S = np.array(
+ [
+ [gamma_gg(N, nf, sx), 0.0 + 0.0j, gamma_gq(N, nf, sx), 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j],
+ [gamma_qg(N, nf, sx), 0.0 + 0.0j, gamma_qq, 0.0 + 0.0j],
+ [0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j, gamma_np_p],
+ ],
+ np.complex_,
+ )
+ return gamma_S
+
+
+@nb.njit(cache=True)
+def gamma_valence_qed(N, nf, sx):
+ r"""Compute the leading-order valence anomalous dimension matrix for the unified evolution basis.
+
+ .. math::
+ \\gamma_V^{(3,0)} = \\left(\begin{array}{cc}
+ \\gamma_{nsV}^{(3,0)} & 0\\
+ 0 & \\gamma_{ns-}^{(3,0)}
+ \\end{array}\right)
+
+ Parameters
+ ----------
+ N : complex
+ Mellin moment
+ nf : int
+ Number of active flavors
+ s1 : complex
+ harmonic sum :math:`S_{1}`
+
+ Returns
+ -------
+ gamma_V : numpy.ndarray
+ Leading-order singlet anomalous dimension matrix :math:`\\gamma_{V}^{(3,0)}(N)`
+
+ See Also
+ --------
+ gamma_ns : :math:`\\gamma_{qq}^{(0)}`
+ gamma_qg : :math:`\\gamma_{qg}^{(0)}`
+ gamma_gq : :math:`\\gamma_{gq}^{(0)}`
+ gamma_gg : :math:`\\gamma_{gg}^{(0)}`
+ """
+ gamma_V = np.array(
+ [
+ [gamma_nsv(N, nf, sx), 0.0],
+ [0.0, gamma_nsm(N, nf, sx)],
+ ],
+ np.complex_,
+ )
+ return gamma_V
diff --git a/src/ekore/harmonics/__init__.py b/src/ekore/harmonics/__init__.py
index 919ca559b..02cbe78c2 100644
--- a/src/ekore/harmonics/__init__.py
+++ b/src/ekore/harmonics/__init__.py
@@ -5,6 +5,7 @@
import numba as nb
import numpy as np
+from . import g_functions, polygamma
from .w1 import S1, Sm1
from .w2 import S2, Sm2
from .w3 import S3, S21, S2m1, Sm2m1, Sm3, Sm21
@@ -217,3 +218,50 @@ def compute_cache(n, max_weight, is_singlet):
sx[3, 1:-1] = s4x(n, sx[:, 0], sx[:, -1], is_singlet)
# return list of list keeping the non zero values
return [[el for el in sx_list if el != 0] for sx_list in sx]
+
+
+@nb.njit(cache=True)
+def compute_qed_ns_cache(n, s1):
+ r"""Get the harmonics sums cache needed for the qed non singlet AD.
+
+ Parameters
+ ----------
+ n : complex
+ Mellin moment
+ s1 : float
+ harmonic sum :math:`S_1(N)`
+
+ Returns
+ -------
+ list
+ harmonic sums cache. It contains:
+
+ .. math ::
+ [S_1(n/2),
+ S_2(n/2),
+ S_{3}(n/2),
+ S_1((n+1)/2),
+ S_2((n+1)/2),
+ S_{3}((n+1)/2),
+ g_3(n),
+ g_3(n+2)]
+
+ """
+ s1h = S1(n / 2.0)
+ sx_qed_ns = [s1h]
+ S2h = S2(n / 2.0)
+ sx_qed_ns.append(S2h)
+ S3h = S3(n / 2.0)
+ sx_qed_ns.append(S3h)
+ S1p1h = S1((n + 1.0) / 2.0)
+ sx_qed_ns.append(S1p1h)
+ S2p1h = S2((n + 1.0) / 2.0)
+ sx_qed_ns.append(S2p1h)
+ S3p1h = S3((n + 1.0) / 2.0)
+ sx_qed_ns.append(S3p1h)
+ g3N = g_functions.mellin_g3(n, s1)
+ sx_qed_ns.append(g3N)
+ S1p2 = polygamma.recursive_harmonic_sum(s1, n, 2, 1)
+ g3Np2 = g_functions.mellin_g3(n + 2.0, S1p2)
+ sx_qed_ns.append(g3Np2)
+ return sx_qed_ns
diff --git a/tests/eko/evolution_operator/test_flavors.py b/tests/eko/evolution_operator/test_flavors.py
index f7cd3d51b..81bb35fc4 100644
--- a/tests/eko/evolution_operator/test_flavors.py
+++ b/tests/eko/evolution_operator/test_flavors.py
@@ -69,6 +69,33 @@ def test_get_range():
assert (4, 4) == flavors.get_range(
[member.MemberName(n) for n in ["S.S", "V3.V3", "T15.T15"]]
)
+ assert (3, 3) == flavors.get_range(
+ [member.MemberName(n) for n in ["S.S", "Td3.Td3"]], True
+ )
+ assert (3, 4) == flavors.get_range(
+ [member.MemberName(n) for n in ["S.S", "Td3.Td3", "Tu3.S"]], True
+ )
+ assert (4, 4) == flavors.get_range(
+ [member.MemberName(n) for n in ["S.S", "Td3.Td3", "Tu3.Tu3"]], True
+ )
+ assert (5, 5) == flavors.get_range(
+ [member.MemberName(n) for n in ["S.S", "Td3.Td3", "Tu3.Tu3", "Td8.Td8"]], True
+ )
+ assert (6, 6) == flavors.get_range(
+ [
+ member.MemberName(n)
+ for n in ["S.S", "Td3.Td3", "Tu3.Tu3", "Td8.Td8", "Tu8.Tu8"]
+ ],
+ True,
+ )
+ with pytest.raises(ValueError):
+ flavors.get_range(
+ [
+ member.MemberName(n)
+ for n in ["S.S", "Td3.Td3", "Tu3.Tu3", "Td8.Td8", "T35.T35"]
+ ],
+ True,
+ )
def test_rotate_pm_to_flavor():
@@ -91,6 +118,27 @@ def test_rotate_matching():
assert len(list(filter(lambda e: "b-" in e, m.keys()))) == 1
+def test_rotate_matching_qed():
+ m = flavors.rotate_matching(4, True)
+ assert len(list(filter(lambda e: "c+" in e, m.keys()))) == 3
+ assert len(list(filter(lambda e: "b-" in e, m.keys()))) == 1
+ # S' = S + c+
+ np.testing.assert_allclose(m["S.S"], 1.0)
+ np.testing.assert_allclose(m["S.c+"], 1.0)
+ # T3u' = u+ - c+ = 1/3(S + Sdelta) - c+
+ np.testing.assert_allclose(m["Tu3.S"], 1.0 / 3.0)
+ np.testing.assert_allclose(m["Tu3.Sdelta"], 1.0 / 3.0)
+ np.testing.assert_allclose(m["Tu3.c+"], -1.0)
+ # Sdelta' = u+ + c+ - d+ - s+ = 1/3(2 Sdelta - S) + c+
+ np.testing.assert_allclose(m["Sdelta.Sdelta"], 2.0 / 3.0)
+ np.testing.assert_allclose(m["Sdelta.S"], -1.0 / 3.0)
+ np.testing.assert_allclose(m["Sdelta.c+"], +1.0)
+
+ m = flavors.rotate_matching(5, True)
+ assert len(list(filter(lambda e: "b-" in e, m.keys()))) == 3
+ assert len(list(filter(lambda e: "t+" in e, m.keys()))) == 1
+
+
def test_rotate_matching_is_inv():
def replace_names(k):
for q in range(4, 6 + 1):
@@ -113,6 +161,34 @@ def load(m):
np.testing.assert_allclose(m @ minv, np.eye(len(br.evol_basis)), atol=1e-10)
+def test_rotate_matching_is_inv_qed():
+ def replace_names(k):
+ names = {3: "d3", 4: "u3", 5: "d8", 6: "u8"}
+ for q in range(4, 6 + 1):
+ k = k.replace(br.quark_names[q - 1] + "+", f"T{names[q]}").replace(
+ br.quark_names[q - 1] + "-", f"V{names[q]}"
+ )
+ return k
+
+ def load(m):
+ mm = np.zeros((len(br.unified_evol_basis), len(br.unified_evol_basis)))
+ for k, v in m.items():
+ k = replace_names(k)
+ kk = k.split(".")
+ mm[
+ br.unified_evol_basis.index(kk[0]), br.unified_evol_basis.index(kk[1])
+ ] = v
+ return mm
+
+ for nf in range(4, 6 + 1):
+ m = load(flavors.rotate_matching(nf, True))
+ minv = load(flavors.rotate_matching_inverse(nf, True))
+ print(m @ minv)
+ np.testing.assert_allclose(
+ m @ minv, np.eye(len(br.unified_evol_basis)), atol=1e-10
+ )
+
+
def test_pids_from_intrinsic_unified_evol():
for nf in range(3, 6 + 1):
labels = br.intrinsic_unified_evol_labels(nf)
diff --git a/tests/eko/evolution_operator/test_init.py b/tests/eko/evolution_operator/test_init.py
index ddda30d13..054407c8a 100644
--- a/tests/eko/evolution_operator/test_init.py
+++ b/tests/eko/evolution_operator/test_init.py
@@ -12,6 +12,7 @@
from eko.interpolation import InterpolatorDispatcher
from eko.io.runcards import OperatorCard, ScaleVariationsMethod, TheoryCard
from eko.kernels import non_singlet as ns
+from eko.kernels import non_singlet_qed as qed_ns
from eko.kernels import singlet as s
@@ -28,8 +29,11 @@ def test_quad_ker_errors():
is_log=True,
logx=0.1,
areas=[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
- as1=1,
- as0=2,
+ as_list=[2.0, 1.0],
+ mu2_from=1.0,
+ mu2_to=2.0,
+ a_half=np.array([[1.5, 0.01]]),
+ alphaem_running=False,
nf=3,
L=0,
ev_op_iterations=1,
@@ -51,85 +55,96 @@ def test_quad_ker(monkeypatch):
monkeypatch.setattr(interpolation, "log_evaluate_Nx", lambda *args: 1)
monkeypatch.setattr(interpolation, "evaluate_Nx", lambda *args: 1)
monkeypatch.setattr(ns, "dispatcher", lambda *args: 1.0)
+ monkeypatch.setattr(qed_ns, "dispatcher", lambda *args: 1.0)
monkeypatch.setattr(s, "dispatcher", lambda *args: np.identity(2))
- for is_log in [True, False]:
- for polarized in [True, False]:
- res_ns = quad_ker(
- u=0,
- order=(1, 0),
- mode0=br.non_singlet_pids_map["ns+"],
- mode1=0,
- method="",
- is_log=is_log,
- logx=0.1,
- areas=[0.1, 0.2, 0.3],
- as1=1,
- as0=2,
- nf=3,
- L=0,
- ev_op_iterations=0,
- ev_op_max_order=(0, 0),
- sv_mode=1,
- is_threshold=False,
- is_polarized=polarized,
- is_time_like=False,
- )
- np.testing.assert_allclose(res_ns, 1.0)
- res_s = quad_ker(
- u=0,
- order=(1, 0),
- mode0=100,
- mode1=100,
- method="",
- is_log=is_log,
- logx=0.123,
- areas=np.zeros(3),
- as1=1,
- as0=2,
- nf=3,
- L=0,
- ev_op_iterations=0,
- ev_op_max_order=(0, 0),
- sv_mode=1,
- is_threshold=False,
- is_polarized=polarized,
- is_time_like=False,
- )
- np.testing.assert_allclose(res_s, 1.0)
- res_s = quad_ker(
- u=0,
- order=(1, 0),
- mode0=100,
- mode1=21,
- method="",
- is_log=is_log,
- logx=0.0,
- areas=np.zeros(3),
- as1=1,
- as0=2,
- nf=3,
- L=0,
- ev_op_iterations=0,
- ev_op_max_order=(0, 0),
- sv_mode=1,
- is_threshold=False,
- is_polarized=polarized,
- is_time_like=False,
- )
- np.testing.assert_allclose(res_s, 0.0)
+ params = [
+ ((1, 0), br.non_singlet_pids_map["ns+"], 0, "", 0.0, 0.0),
+ ((3, 1), br.non_singlet_pids_map["ns+u"], 0, "", 0.0, 0.0),
+ ((1, 0), 100, 100, "", 0.123, 1.0),
+ ((1, 0), 100, 21, "", 0.0, 0.0),
+ ((1, 1), 100, 100, "iterate-exact", 0.123, 1.0),
+ ((1, 1), 100, 21, "iterate-exact", 0.123, 0.0),
+ ((1, 1), 10200, 10200, "iterate-exact", 0.123, 1.0),
+ ((1, 1), 10200, 10204, "iterate-exact", 0.123, 0.0),
+ ]
+ for order, mode0, mode1, method, logx, res in params:
+ for is_log in [True, False]:
+ for polarized in [True, False]:
+ res_ns = quad_ker(
+ u=0,
+ order=order,
+ mode0=mode0,
+ mode1=mode1,
+ method=method,
+ is_log=is_log,
+ logx=logx,
+ areas=np.zeros(3),
+ as_list=[2.0, 1.0],
+ mu2_from=1,
+ mu2_to=2,
+ a_half=np.array([[1.5, 0.01]]),
+ alphaem_running=False,
+ nf=3,
+ L=0,
+ ev_op_iterations=0,
+ ev_op_max_order=(0, 0),
+ sv_mode=1,
+ is_threshold=False,
+ is_polarized=polarized,
+ is_time_like=False,
+ )
+ np.testing.assert_allclose(res_ns, res)
for label in [(br.non_singlet_pids_map["ns+"], 0), (100, 100)]:
+ for sv in [2, 3]:
+ for polarized in [True, False]:
+ res_sv = quad_ker(
+ u=0,
+ order=(1, 0),
+ mode0=label[0],
+ mode1=label[1],
+ method="",
+ is_log=True,
+ logx=0.123,
+ areas=np.zeros(3),
+ as_list=[2.0, 1.0],
+ mu2_from=1,
+ mu2_to=2,
+ a_half=np.array([[1.5, 0.01]]),
+ alphaem_running=False,
+ nf=3,
+ L=0,
+ ev_op_iterations=0,
+ ev_op_max_order=(1, 0),
+ sv_mode=sv,
+ is_threshold=False,
+ is_polarized=polarized,
+ is_time_like=False,
+ )
+ np.testing.assert_allclose(res_sv, 1.0)
+ for label in [
+ (100, 100),
+ (21, 21),
+ (22, 22),
+ (101, 101),
+ (10200, 10200),
+ (10204, 10204),
+ (10202, 0),
+ ]:
for sv in [2, 3]:
res_sv = quad_ker(
u=0,
- order=(1, 0),
+ order=(1, 1),
mode0=label[0],
mode1=label[1],
- method="",
+ method="iterate-exact",
is_log=True,
logx=0.123,
areas=np.zeros(3),
- as1=1,
- as0=2,
+ as_list=[2.0, 1.0],
+ mu2_from=1,
+ mu2_to=2,
+ a_half=np.array([[1.5, 0.01]]),
+ alphaem_running=False,
nf=3,
L=0,
ev_op_iterations=0,
@@ -151,8 +166,11 @@ def test_quad_ker(monkeypatch):
is_log=True,
logx=0.0,
areas=np.zeros(3),
- as1=1,
- as0=2,
+ as_list=[2.0, 1.0],
+ mu2_from=1,
+ mu2_to=2,
+ a_half=np.array([[1.5, 0.01]]),
+ alphaem_running=False,
nf=3,
L=0,
ev_op_iterations=0,
@@ -165,6 +183,21 @@ def test_quad_ker(monkeypatch):
np.testing.assert_allclose(res_ns, 0.0)
+class FakeCoupling:
+ def __init__(self):
+ self.alphaem_running = None
+ self.q2_ref = 0.0
+
+ def a(self, scale_to=None, fact_scale=None, nf_to=None):
+ return (0.1, 0.01)
+
+ def compute(self, a_ref, nf, scale_from, scale_to):
+ return a_ref
+
+
+fake_managers = {"couplings": FakeCoupling()}
+
+
class TestOperator:
def test_labels(self):
o = Operator(
@@ -173,8 +206,10 @@ def test_labels(self):
debug_skip_non_singlet=False,
debug_skip_singlet=False,
n_integration_cores=1,
+ ModSV=None,
+ ev_op_iterations=1
),
- {},
+ fake_managers,
3,
1,
2,
@@ -186,8 +221,42 @@ def test_labels(self):
debug_skip_non_singlet=True,
debug_skip_singlet=True,
n_integration_cores=1,
+ ModSV=None,
+ ev_op_iterations=1
+ ),
+ fake_managers,
+ 3,
+ 1,
+ 2,
+ )
+ assert sorted(o.labels) == []
+
+ def test_labels_qed(self):
+ o = Operator(
+ dict(
+ order=(3, 1),
+ debug_skip_non_singlet=False,
+ debug_skip_singlet=False,
+ n_integration_cores=1,
+ ModSV=None,
+ ev_op_iterations=1,
+ ),
+ fake_managers,
+ 3,
+ 1,
+ 2,
+ )
+ assert sorted(o.labels) == sorted(br.full_unified_labels)
+ o = Operator(
+ dict(
+ order=(2, 1),
+ debug_skip_non_singlet=True,
+ debug_skip_singlet=True,
+ n_integration_cores=1,
+ ModSV=None,
+ ev_op_iterations=1,
),
- {},
+ fake_managers,
3,
1,
2,
@@ -205,8 +274,10 @@ def test_n_pools(self):
debug_skip_non_singlet=True,
debug_skip_singlet=True,
n_integration_cores=-excluded_cores,
+ ModSV=None,
+ ev_op_iterations=1
),
- {},
+ fake_managers,
3,
1,
10,
@@ -316,6 +387,18 @@ def test_compute(self, monkeypatch, theory_ffns, operator_card, tmp_path):
err_msg=k,
)
+ for n in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ g.config["order"] = (n, qed)
+ o1 = Operator(g.config, g.managers, 3, 2.0, 2.0)
+ # o1.config["order"] = (n, qed)
+ o1.compute()
+ for k in br.non_singlet_unified_labels:
+ assert k in o1.op_members
+ np.testing.assert_allclose(
+ o1.op_members[k].value, np.eye(4), err_msg=k
+ )
+
def test_pegasus_path():
def quad_ker_pegasus(
@@ -349,6 +432,8 @@ def quad_ker_pegasus(
logxs = np.log(int_disp.xgrid.raw)
a1 = 1
a0 = 2
+ mu2_from = 1
+ mu2_to = 2**2
nf = 3
L = 0
ev_op_iterations = 10
@@ -366,8 +451,11 @@ def quad_ker_pegasus(
int_disp.log,
logx,
bf.areas_representation,
- a1,
- a0,
+ [a0, a1],
+ mu2_from,
+ mu2_to,
+ [[(a1 + a0) / 2, 0.00058]],
+ False,
nf,
L,
ev_op_iterations,
diff --git a/tests/eko/evolution_operator/test_matchin_condition.py b/tests/eko/evolution_operator/test_matchin_condition.py
index cf6ddd0f5..8cbd6b537 100644
--- a/tests/eko/evolution_operator/test_matchin_condition.py
+++ b/tests/eko/evolution_operator/test_matchin_condition.py
@@ -124,3 +124,93 @@ def test_split_ad_to_evol_map(self):
d.op_members[member.MemberName("b+.g")].value,
ome[(br.matching_hplus_pid, 21)].value,
)
+
+ def test_split_ad_to_evol_map_qed(self):
+ ome = self.mkOME()
+ a = MatchingCondition.split_ad_to_evol_map(ome, 3, 1, [], qed=True)
+ triv_keys = [
+ "ph.ph",
+ "S.S",
+ "S.g",
+ "g.S",
+ "g.g",
+ "Sdelta.Sdelta",
+ "V.V",
+ "Vdelta.Vdelta",
+ "Td3.Td3",
+ "Vd3.Vd3",
+ ]
+ # nf = 3
+ keys3 = [
+ "c+.S",
+ "c+.g",
+ # "c-.V",
+ ]
+ assert sorted(str(k) for k in a.op_members.keys()) == sorted(
+ [*triv_keys, *keys3]
+ )
+ assert_almost_equal(
+ a.op_members[member.MemberName("V.V")].value,
+ ome[(200, 200)].value,
+ )
+ # # if alpha is zero, nothing non-trivial should happen
+ b = MatchingCondition.split_ad_to_evol_map(ome, 3, 1, [], qed=True)
+ assert sorted(str(k) for k in b.op_members.keys()) == sorted(
+ [*triv_keys, *keys3]
+ )
+ # assert_almost_equal(
+ # b.op_members[member.MemberName("V.V")].value,
+ # np.eye(self.shape[0]),
+ # )
+ # nf=3 + IC
+ self.update_intrinsic_OME(ome)
+ c = MatchingCondition.split_ad_to_evol_map(ome, 3, 1, [4], qed=True)
+ assert sorted(str(k) for k in c.op_members.keys()) == sorted(
+ [*triv_keys, *keys3, "S.c+", "g.c+", "c+.c+", "c-.c-"]
+ )
+ assert_almost_equal(
+ c.op_members[member.MemberName("V.V")].value,
+ b.op_members[member.MemberName("V.V")].value,
+ )
+ # nf=3 + IB
+ d = MatchingCondition.split_ad_to_evol_map(ome, 3, 1, [5], qed=True)
+ assert sorted(str(k) for k in d.op_members.keys()) == sorted(
+ [*triv_keys, *keys3, "b+.b+", "b-.b-"]
+ )
+ assert_almost_equal(
+ d.op_members[member.MemberName("b+.b+")].value,
+ np.eye(self.shape[0]),
+ )
+ # nf=4 + IB
+ d = MatchingCondition.split_ad_to_evol_map(ome, 4, 1, [5], qed=True)
+ assert sorted(str(k) for k in d.op_members.keys()) == sorted(
+ [
+ *triv_keys,
+ "Tu3.Tu3",
+ "Vu3.Vu3",
+ "S.b+",
+ "g.b+",
+ # "V.b-",
+ "b+.S",
+ "b+.g",
+ "b+.b+",
+ # "b-.V",
+ "b-.b-",
+ ]
+ )
+ assert_almost_equal(
+ d.op_members[member.MemberName("V.V")].value,
+ a.op_members[member.MemberName("V.V")].value,
+ )
+ # assert_almost_equal(
+ # d.op_members[member.MemberName("b-.V")].value,
+ # ome[(br.matching_hminus_pid, 200)].value,
+ # )
+ assert_almost_equal(
+ d.op_members[member.MemberName("b+.S")].value,
+ ome[(br.matching_hplus_pid, 100)].value,
+ )
+ assert_almost_equal(
+ d.op_members[member.MemberName("b+.g")].value,
+ ome[(br.matching_hplus_pid, 21)].value,
+ )
diff --git a/tests/eko/evolution_operator/test_physical.py b/tests/eko/evolution_operator/test_physical.py
index a194f0502..b4a9936b6 100644
--- a/tests/eko/evolution_operator/test_physical.py
+++ b/tests/eko/evolution_operator/test_physical.py
@@ -166,19 +166,64 @@ def test_to_flavor_basis_tensor_gg(self):
np.testing.assert_allclose(vt[7, :, :7, :], 0)
np.testing.assert_allclose(vt[8:, :, 7, :], 0)
+ shape = (4, 4)
-def mk_op_members(shape=(2, 2)):
- m = np.random.rand(len(br.full_labels), *shape)
- e = np.random.rand(len(br.full_labels), *shape)
+ def test_to_flavor_basis_tensor_ss_qed(self):
+ (SS,) = self._mkOM(1)
+ a = PhysicalOperator(
+ dict(
+ zip(
+ self._mkNames(("S.S",)),
+ (SS,),
+ )
+ ),
+ 1,
+ )
+ vt, _ = a.to_flavor_basis_tensor(qed=True)
+ np.testing.assert_allclose(vt[6, :, 6, :], vt[6, :, 5, :])
+ np.testing.assert_allclose(vt[6, :, 6, :], vt[5, :, 6, :])
+ np.testing.assert_allclose(vt[6, :, 6, :], SS.value[:, :] / (2 * 3))
+ np.testing.assert_allclose(vt[1, :, :, :], 0)
+ np.testing.assert_allclose(vt[:, :, 1, :], 0)
+ np.testing.assert_allclose(vt[7, :, :, :], 0)
+ np.testing.assert_allclose(vt[:, :, 7, :], 0)
+
+ def test_to_flavor_basis_tensor_gg_qed(self):
+ (gg,) = self._mkOM(1)
+ a = PhysicalOperator(
+ dict(
+ zip(
+ self._mkNames(("g.g",)),
+ (gg,),
+ )
+ ),
+ 1,
+ )
+ vt, _ = a.to_flavor_basis_tensor()
+ np.testing.assert_allclose(vt[6, :, 6, :], 0)
+ np.testing.assert_allclose(vt[7, :, 7, :], gg.value[:, :])
+ np.testing.assert_allclose(vt[1, :, :, :], 0)
+ np.testing.assert_allclose(vt[:, :, 1, :], 0)
+ np.testing.assert_allclose(vt[7, :, :7, :], 0)
+ np.testing.assert_allclose(vt[8:, :, 7, :], 0)
+
+
+def mk_op_members(shape=(2, 2), qed=False):
+ if not qed:
+ full_labels = br.full_labels
+ else:
+ full_labels = br.full_unified_labels
+ m = np.random.rand(len(full_labels), *shape)
+ e = np.random.rand(len(full_labels), *shape)
om = {}
- for j, l in enumerate(br.full_labels):
+ for j, l in enumerate(full_labels):
om[l] = member.OpMember(m[j], e[j])
return om
-def get_ad_to_evol_map(nf, intrinsic_range=None):
- oms = mk_op_members()
- m = PhysicalOperator.ad_to_evol_map(oms, nf, 1, intrinsic_range)
+def get_ad_to_evol_map(nf, intrinsic_range=None, qed=False):
+ oms = mk_op_members(qed=qed)
+ m = PhysicalOperator.ad_to_evol_map(oms, nf, 1, intrinsic_range, qed)
return sorted(map(str, m.op_members.keys()))
@@ -201,3 +246,47 @@ def test_ad_to_evol_map():
assert sorted(
[*triv_ops, "T15.T15", "V15.V15", "T24.T24", "V24.V24", "T35.T35", "V35.V35"]
) == get_ad_to_evol_map(6)
+
+
+def test_ad_to_evol_map_qed():
+ triv_ops = (
+ "g.g",
+ "g.ph",
+ "g.S",
+ "g.Sdelta",
+ "ph.g",
+ "ph.ph",
+ "ph.S",
+ "ph.Sdelta",
+ "S.g",
+ "S.ph",
+ "S.S",
+ "S.Sdelta",
+ "Sdelta.g",
+ "Sdelta.ph",
+ "Sdelta.S",
+ "Sdelta.Sdelta",
+ "V.V",
+ "V.Vdelta",
+ "Vdelta.V",
+ "Vdelta.Vdelta",
+ "Vd3.Vd3",
+ "Td3.Td3",
+ )
+ # nf=3
+ assert sorted(triv_ops) == get_ad_to_evol_map(3, qed=True)
+ # nf=3 + IC
+ assert sorted([*triv_ops, "c+.c+", "c-.c-"]) == get_ad_to_evol_map(3, [4], qed=True)
+ # nf=3 + IC + IB
+ assert sorted(
+ [*triv_ops, "c+.c+", "c-.c-", "b+.b+", "b-.b-"]
+ ) == get_ad_to_evol_map(3, [4, 5], qed=True)
+ # nf=4 + IC(non-existant) + IB
+ ks = sorted([*triv_ops, "Vu3.Vu3", "Tu3.Tu3", "b+.b+", "b-.b-"])
+ assert ks == get_ad_to_evol_map(4, [4, 5], qed=True)
+ # nf=4 + IB
+ assert ks == get_ad_to_evol_map(4, [5], qed=True)
+ # nf=6
+ assert sorted(
+ [*triv_ops, "Tu3.Tu3", "Vu3.Vu3", "Td8.Td8", "Vd8.Vd8", "Tu8.Tu8", "Vu8.Vu8"]
+ ) == get_ad_to_evol_map(6, qed=True)
diff --git a/tests/eko/kernels/test_as4_ei.py b/tests/eko/kernels/test_as4_ei.py
index 95dc8a9ce..acfec184e 100644
--- a/tests/eko/kernels/test_as4_ei.py
+++ b/tests/eko/kernels/test_as4_ei.py
@@ -2,7 +2,7 @@
from eko import beta
from eko.kernels import as4_evolution_integrals as as4_ei
-from eko.kernels.evolution_integrals import j00
+from eko.kernels.evolution_integrals import j12
def test_zero():
@@ -81,8 +81,8 @@ def test_der_n3lo_exa():
# 03
rhs = 1.0 / (a1 * den)
- j00p = j00(a1 + 0.5 * delta_a, a0, nf)
- j00m = j00(a1 - 0.5 * delta_a, a0, nf)
+ j00p = j12(a1 + 0.5 * delta_a, a0, beta0)
+ j00m = j12(a1 - 0.5 * delta_a, a0, beta0)
lhs = (
as4_ei.j03_exact(j00p, j13p, j23p, j33p, b_list)
- as4_ei.j03_exact(j00m, j13m, j23m, j33m, b_list)
@@ -129,8 +129,8 @@ def test_der_n3lo_exp():
# 03
rhs = 1.0 / (a1 * den)
- j00p = j00(a1 + 0.5 * delta_a, a0, nf)
- j00m = j00(a1 - 0.5 * delta_a, a0, nf)
+ j00p = j12(a1 + 0.5 * delta_a, a0, beta0)
+ j00m = j12(a1 - 0.5 * delta_a, a0, beta0)
lhs = (
as4_ei.j03_expanded(j00p, j13p, j23p, j33p, b_list)
- as4_ei.j03_expanded(j00m, j13m, j23m, j33m, b_list)
diff --git a/tests/eko/kernels/test_ei.py b/tests/eko/kernels/test_ei.py
index 56da36a53..5c89bc6e0 100644
--- a/tests/eko/kernels/test_ei.py
+++ b/tests/eko/kernels/test_ei.py
@@ -7,31 +7,59 @@
def test_zero():
"""No evolution results in exp(0)"""
nf = 3
+ beta0 = beta.beta_qcd((2, 0), nf)
+ b_vec = [beta.beta_qcd((2 + i, 0), nf) / beta0 for i in range(0, 2 + 1)]
for fnc in [
- ei.j00,
- ei.j01_exact,
- ei.j01_expanded,
- ei.j11_exact,
- ei.j11_expanded,
- ei.j02_exact,
- ei.j02_expanded,
- ei.j12_exact,
- ei.j12_expanded,
- ei.j22_exact,
- ei.j22_expanded,
+ ei.j13_exact,
+ ei.j13_expanded,
+ ei.j23_exact,
+ ei.j14_exact,
+ ei.j14_expanded,
+ ei.j24_exact,
+ ei.j24_expanded,
+ ei.j34_exact,
]:
- np.testing.assert_allclose(fnc(1, 1, nf), 0)
+ np.testing.assert_allclose(fnc(1, 1, beta0, b_vec), 0)
+ for fnc in [
+ ei.j12,
+ ei.j23_expanded,
+ ei.j34_expanded,
+ ]:
+ np.testing.assert_allclose(fnc(1, 1, beta0), 0)
+
+
+def test_zero_qed():
+ """No evolution results in exp(0)"""
+ aem = 0.00058
+ nf = 3
+ beta0 = beta.beta_qcd((2, 0), nf) + aem * beta.beta_qcd((2, 1), nf)
+ b_vec = [beta.beta_qcd((2 + i, 0), nf) / beta0 for i in range(0, 2 + 1)]
+ for fnc in [
+ ei.j23_exact,
+ ei.j13_exact,
+ ei.j34_exact,
+ ei.j24_exact,
+ ei.j14_exact,
+ ]:
+ np.testing.assert_allclose(fnc(1, 1, beta0, b_vec), 0)
+ for fnc in [
+ ei.j12,
+ ei.j23_expanded,
+ ei.j34_expanded,
+ ]:
+ np.testing.assert_allclose(fnc(1, 1, beta0), 0)
def test_der_lo():
"""LO derivative"""
nf = 3
+ beta0 = beta.beta_qcd((2, 0), nf)
a0 = 5
a1 = 3
delta_a = -1e-6
rhs = 1.0 / (beta.beta_qcd((2, 0), nf) * a1)
lhs = (
- ei.j00(a1 + 0.5 * delta_a, a0, nf) - ei.j00(a1 - 0.5 * delta_a, a0, nf)
+ ei.j12(a1 + 0.5 * delta_a, a0, beta0) - ei.j12(a1 - 0.5 * delta_a, a0, beta0)
) / delta_a
np.testing.assert_allclose(rhs, lhs)
@@ -39,14 +67,16 @@ def test_der_lo():
def test_der_nlo_exp():
"""expanded NLO derivative"""
nf = 3
+ beta0 = beta.beta_qcd((2, 0), nf)
+ b_vec = [beta.beta_qcd((2 + i, 0), nf) / beta0 for i in range(0, 2 + 1)]
a0 = 0.3
a1 = 0.1
delta_a = -1e-6
# 01
rhs = 1.0 / (beta.beta_qcd((2, 0), nf) * a1 + beta.beta_qcd((3, 0), nf) * a1**2)
lhs = (
- ei.j01_expanded(a1 + 0.5 * delta_a, a0, nf)
- - ei.j01_expanded(a1 - 0.5 * delta_a, a0, nf)
+ ei.j13_expanded(a1 + 0.5 * delta_a, a0, beta0, b_vec)
+ - ei.j13_expanded(a1 - 0.5 * delta_a, a0, beta0, b_vec)
) / delta_a
np.testing.assert_allclose(
rhs, lhs, atol=np.abs((beta.b_qcd((3, 0), nf) * a1) ** 2)
@@ -54,8 +84,8 @@ def test_der_nlo_exp():
# 11
rhs = 1.0 / (beta.beta_qcd((2, 0), nf) + beta.beta_qcd((3, 0), nf) * a1)
lhs = (
- ei.j11_expanded(a1 + 0.5 * delta_a, a0, nf)
- - ei.j11_expanded(a1 - 0.5 * delta_a, a0, nf)
+ ei.j23_expanded(a1 + 0.5 * delta_a, a0, beta0)
+ - ei.j23_expanded(a1 - 0.5 * delta_a, a0, beta0)
) / delta_a
np.testing.assert_allclose(rhs, lhs, atol=np.abs(beta.b_qcd((3, 0), nf) * a1))
@@ -63,21 +93,23 @@ def test_der_nlo_exp():
def test_der_nlo_exa():
"""exact NLO derivative"""
nf = 3
+ beta0 = beta.beta_qcd((2, 0), nf)
+ b_vec = [beta.beta_qcd((2 + i, 0), nf) / beta0 for i in range(0, 2 + 1)]
a0 = 0.3
a1 = 0.1
delta_a = -1e-6
# 01
rhs = 1.0 / (beta.beta_qcd((2, 0), nf) * a1 + beta.beta_qcd((3, 0), nf) * a1**2)
lhs = (
- ei.j01_exact(a1 + 0.5 * delta_a, a0, nf)
- - ei.j01_exact(a1 - 0.5 * delta_a, a0, nf)
+ ei.j13_exact(a1 + 0.5 * delta_a, a0, beta0, b_vec)
+ - ei.j13_exact(a1 - 0.5 * delta_a, a0, beta0, b_vec)
) / delta_a
np.testing.assert_allclose(rhs, lhs, atol=np.abs(delta_a)) # in fact O(delta_a^2)
# 11
rhs = 1.0 / (beta.beta_qcd((2, 0), nf) + beta.beta_qcd((3, 0), nf) * a1)
lhs = (
- ei.j11_exact(a1 + 0.5 * delta_a, a0, nf)
- - ei.j11_exact(a1 - 0.5 * delta_a, a0, nf)
+ ei.j23_exact(a1 + 0.5 * delta_a, a0, beta0, b_vec)
+ - ei.j23_exact(a1 - 0.5 * delta_a, a0, beta0, b_vec)
) / delta_a
np.testing.assert_allclose(rhs, lhs, atol=np.abs(delta_a)) # in fact O(delta_a^2)
@@ -85,6 +117,8 @@ def test_der_nlo_exa():
def test_der_nnlo_exp():
"""expanded NNLO derivative"""
nf = 3
+ beta0 = beta.beta_qcd((2, 0), nf)
+ b_vec = [beta.beta_qcd((2 + i, 0), nf) / beta0 for i in range(0, 2 + 1)]
a0 = 0.3
a1 = 0.1
delta_a = -1e-6
@@ -99,8 +133,8 @@ def test_der_nnlo_exp():
+ beta.beta_qcd((4, 0), nf) * a1**3
)
lhs = (
- ei.j02_expanded(a1 + 0.5 * delta_a, a0, nf)
- - ei.j02_expanded(a1 - 0.5 * delta_a, a0, nf)
+ ei.j14_expanded(a1 + 0.5 * delta_a, a0, beta0, b_vec)
+ - ei.j14_expanded(a1 - 0.5 * delta_a, a0, beta0, b_vec)
) / delta_a
toll = (
(
@@ -118,8 +152,8 @@ def test_der_nnlo_exp():
+ beta.beta_qcd((4, 0), nf) * a1**2
)
lhs = (
- ei.j12_expanded(a1 + 0.5 * delta_a, a0, nf)
- - ei.j12_expanded(a1 - 0.5 * delta_a, a0, nf)
+ ei.j24_expanded(a1 + 0.5 * delta_a, a0, beta0, b_vec)
+ - ei.j24_expanded(a1 - 0.5 * delta_a, a0, beta0, b_vec)
) / delta_a
toll = (
(beta.b_qcd((3, 0), nf) ** 2 - beta.b_qcd((4, 0), nf))
@@ -134,8 +168,8 @@ def test_der_nnlo_exp():
+ beta.beta_qcd((4, 0), nf) * a1**2
)
lhs = (
- ei.j22_expanded(a1 + 0.5 * delta_a, a0, nf)
- - ei.j22_expanded(a1 - 0.5 * delta_a, a0, nf)
+ ei.j34_expanded(a1 + 0.5 * delta_a, a0, beta0)
+ - ei.j34_expanded(a1 - 0.5 * delta_a, a0, beta0)
) / delta_a
np.testing.assert_allclose(
rhs,
@@ -147,6 +181,8 @@ def test_der_nnlo_exp():
def test_der_nnlo_exa():
"""exact NNLO derivative"""
nf = 3
+ beta0 = beta.beta_qcd((2, 0), nf)
+ b_vec = [beta.beta_qcd((2 + i, 0), nf) / beta0 for i in range(0, 2 + 1)]
a0 = 0.3
a1 = 0.1
delta_a = -1e-6
@@ -157,8 +193,8 @@ def test_der_nnlo_exa():
+ beta.beta_qcd((4, 0), nf) * a1**3
)
lhs = (
- ei.j02_exact(a1 + 0.5 * delta_a, a0, nf)
- - ei.j02_exact(a1 - 0.5 * delta_a, a0, nf)
+ ei.j14_exact(a1 + 0.5 * delta_a, a0, beta0, b_vec)
+ - ei.j14_exact(a1 - 0.5 * delta_a, a0, beta0, b_vec)
) / delta_a
np.testing.assert_allclose(rhs, lhs, atol=np.abs(delta_a)) # in fact O(delta_a^2)
# 12
@@ -168,8 +204,8 @@ def test_der_nnlo_exa():
+ beta.beta_qcd((4, 0), nf) * a1**2
)
lhs = (
- ei.j12_exact(a1 + 0.5 * delta_a, a0, nf)
- - ei.j12_exact(a1 - 0.5 * delta_a, a0, nf)
+ ei.j24_exact(a1 + 0.5 * delta_a, a0, beta0, b_vec)
+ - ei.j24_exact(a1 - 0.5 * delta_a, a0, beta0, b_vec)
) / delta_a
np.testing.assert_allclose(rhs, lhs, atol=np.abs(delta_a)) # in fact O(delta_a^2)
# 12
@@ -179,7 +215,7 @@ def test_der_nnlo_exa():
+ beta.beta_qcd((4, 0), nf) * a1**2
)
lhs = (
- ei.j22_exact(a1 + 0.5 * delta_a, a0, nf)
- - ei.j22_exact(a1 - 0.5 * delta_a, a0, nf)
+ ei.j34_exact(a1 + 0.5 * delta_a, a0, beta0, b_vec)
+ - ei.j34_exact(a1 - 0.5 * delta_a, a0, beta0, b_vec)
) / delta_a
np.testing.assert_allclose(rhs, lhs, atol=np.abs(delta_a)) # in fact O(delta_a^2)
diff --git a/tests/eko/kernels/test_kernels_QEDns.py b/tests/eko/kernels/test_kernels_QEDns.py
new file mode 100644
index 000000000..bb6208363
--- /dev/null
+++ b/tests/eko/kernels/test_kernels_QEDns.py
@@ -0,0 +1,327 @@
+import warnings
+
+import numpy as np
+import pytest
+
+from eko import basis_rotation as br
+from eko import beta
+from eko.kernels import non_singlet_qed as ns
+from ekore.anomalous_dimensions.unpolarized import space_like as ad
+
+methods = [
+ # "iterate-expanded",
+ # "decompose-expanded",
+ # "perturbative-expanded",
+ # "truncated",
+ # "ordered-truncated",
+ "iterate-exact",
+ # "decompose-exact",
+ # "perturbative-exact",
+]
+
+
+def test_zero():
+ """No evolution results in exp(0)"""
+ nf = 3
+ ev_op_iterations = 2
+ running_alpha = [True, False]
+ for qcd in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ order = (qcd, qed)
+ gamma_ns = (
+ np.random.rand(qcd + 1, qed + 1) + np.random.rand(qcd + 1, qed + 1) * 1j
+ )
+ for method in methods:
+ for running in running_alpha:
+ np.testing.assert_allclose(
+ ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ [1.0, 1.0, 1.0],
+ [1.0, 1.0],
+ running,
+ nf,
+ ev_op_iterations,
+ 1.0,
+ 1.0,
+ ),
+ 1.0,
+ )
+ np.testing.assert_allclose(
+ ns.dispatcher(
+ order,
+ method,
+ np.zeros((qcd + 1, qed + 1), dtype=complex),
+ [1.0, 1.5, 2.0],
+ [1.0, 1.0],
+ running,
+ nf,
+ ev_op_iterations,
+ 1.0,
+ 1.0,
+ ),
+ 1.0,
+ )
+
+
+def test_zero_true_gamma():
+ """No evolution results in exp(0)"""
+ nf = 3
+ ev_op_iterations = 2
+ running_alpha = [True, False]
+ for mode in br.non_singlet_pids_map.values():
+ if mode in [10201, 10101, 10200]:
+ continue
+ for qcd in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ order = (qcd, qed)
+ n = np.random.rand()
+ gamma_ns = ad.gamma_ns_qed(order, mode, n, nf)
+ for method in methods:
+ for running in running_alpha:
+ np.testing.assert_allclose(
+ ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ [1.0, 1.0, 1.0],
+ [1.0, 1.0],
+ running,
+ nf,
+ ev_op_iterations,
+ 1.0,
+ 1.0,
+ ),
+ 1.0,
+ )
+ np.testing.assert_allclose(
+ ns.dispatcher(
+ order,
+ method,
+ np.zeros((qcd + 1, qed + 1), dtype=complex),
+ [1.0, 1.5, 2.0],
+ [1.0, 1.0],
+ running,
+ nf,
+ ev_op_iterations,
+ 1.0,
+ 1.0,
+ ),
+ 1.0,
+ )
+
+
+from math import nan
+
+from eko.couplings import Couplings, couplings_mod_ev
+from eko.io.types import (
+ CouplingEvolutionMethod,
+ CouplingsRef,
+ EvolutionMethod,
+ MatchingScales,
+ QuarkMassSchemes,
+)
+
+alpharef = (0.118, 0.00781)
+masses = [m**2 for m in (2.0, 4.5, 175.0)]
+muref = 91.0
+couplings = CouplingsRef.from_dict(
+ dict(
+ alphas=[alpharef[0], muref],
+ alphaem=[alpharef[1], nan],
+ num_flavs_ref=5,
+ max_num_flavs=6,
+ )
+)
+evmod = CouplingEvolutionMethod.EXACT
+
+
+def test_ode():
+ ev_op_iterations = 1
+ aem_list = [0.00781] * ev_op_iterations
+ nf = 5
+ delta_mu2 = 1e-6
+ mu2_0 = 5.0**2
+ for mode in br.non_singlet_pids_map.values():
+ if mode in [10201, 10101, 10200]:
+ continue
+ for qcd in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ order = (qcd, qed)
+ sc = Couplings(
+ couplings,
+ order,
+ evmod,
+ masses,
+ hqm_scheme=QuarkMassSchemes.POLE,
+ thresholds_ratios=[1.0, 1.0, 1.0],
+ )
+ a0 = sc.a_s(mu2_0)
+ for mu2_to in [10**2, 15**2]:
+ dlog_mu2 = np.log(mu2_to + 0.5 * delta_mu2) - np.log(
+ mu2_to - 0.5 * delta_mu2
+ )
+ a1 = sc.a_s(mu2_to)
+ gamma_ns = (
+ np.random.rand(3 + 1, 2 + 1) + np.random.rand(3 + 1, 2 + 1) * 1j
+ )
+ gamma_ns[0, 0] = 0.0
+ gamma_ns[2, 1] = 0.0
+ gamma_ns[3, 1] = 0.0
+ gamma_ns[1, 2] = 0.0
+ gamma_ns[2, 2] = 0.0
+ gamma_ns[3, 2] = 0.0
+ gammatot = 0.0
+ for i in range(0, order[0] + 1):
+ for j in range(0, order[1] + 1):
+ gammatot += gamma_ns[i, j] * a1**i * aem_list[0] ** j
+ r = -gammatot
+ for method in methods:
+ rhs = r * ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ np.geomspace(a0, a1, ev_op_iterations + 1),
+ aem_list,
+ False,
+ nf,
+ ev_op_iterations,
+ mu2_to,
+ mu2_0,
+ )
+ lhs = (
+ ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ np.geomspace(
+ a0,
+ sc.a_s(mu2_to + 0.5 * delta_mu2),
+ ev_op_iterations + 1,
+ ),
+ aem_list,
+ False,
+ nf,
+ ev_op_iterations,
+ mu2_to + 0.5 * delta_mu2,
+ mu2_0,
+ )
+ - ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ np.geomspace(
+ a0,
+ sc.a_s(mu2_to - 0.5 * delta_mu2),
+ ev_op_iterations + 1,
+ ),
+ aem_list,
+ False,
+ nf,
+ ev_op_iterations,
+ mu2_to - 0.5 * delta_mu2,
+ mu2_0,
+ )
+ ) / dlog_mu2
+ np.testing.assert_allclose(lhs, rhs, atol=np.abs(1e-3))
+
+
+def test_ode_true_gamma():
+ ev_op_iterations = 1
+ aem_list = [0.00781] * ev_op_iterations
+ nf = 5
+ delta_mu2 = 1e-6
+ mu2_0 = 5.0**2
+ for mode in br.non_singlet_pids_map.values():
+ if mode in [10201, 10101, 10200]:
+ continue
+ for qcd in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ order = (qcd, qed)
+ sc = Couplings(
+ couplings,
+ order,
+ evmod,
+ masses,
+ hqm_scheme=QuarkMassSchemes.POLE,
+ thresholds_ratios=[1.0, 1.0, 1.0],
+ )
+ a0 = sc.a_s(mu2_0)
+ for mu2_to in [10**2, 15**2]:
+ dlog_mu2 = np.log(mu2_to + 0.5 * delta_mu2) - np.log(
+ mu2_to - 0.5 * delta_mu2
+ )
+ a1 = sc.a_s(mu2_to)
+ n = 3 + np.random.rand()
+ gamma_ns = ad.gamma_ns_qed(order, mode, n, nf)
+ gammatot = 0.0
+ for i in range(0, order[0] + 1):
+ for j in range(0, order[1] + 1):
+ gammatot += gamma_ns[i, j] * a1**i * aem_list[0] ** j
+ r = -gammatot
+ for method in methods:
+ rhs = r * ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ np.geomspace(a0, a1, ev_op_iterations + 1),
+ aem_list,
+ False,
+ nf,
+ ev_op_iterations,
+ mu2_to,
+ mu2_0,
+ )
+ lhs = (
+ ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ np.geomspace(
+ a0,
+ sc.a_s(mu2_to + 0.5 * delta_mu2),
+ ev_op_iterations + 1,
+ ),
+ aem_list,
+ False,
+ nf,
+ ev_op_iterations,
+ mu2_to + 0.5 * delta_mu2,
+ mu2_0,
+ )
+ - ns.dispatcher(
+ order,
+ method,
+ gamma_ns,
+ np.geomspace(
+ a0,
+ sc.a_s(mu2_to - 0.5 * delta_mu2),
+ ev_op_iterations + 1,
+ ),
+ aem_list,
+ False,
+ nf,
+ ev_op_iterations,
+ mu2_to - 0.5 * delta_mu2,
+ mu2_0,
+ )
+ ) / dlog_mu2
+ np.testing.assert_allclose(lhs, rhs, atol=np.abs(1e-3))
+
+
+def test_error():
+ for running in [True, False]:
+ with pytest.raises(NotImplementedError):
+ ns.dispatcher(
+ (4, 2),
+ "iterate-exact",
+ np.random.rand(4, 3),
+ [0.1, 0.2],
+ [0.01],
+ running,
+ 3,
+ 10,
+ 1.0,
+ 1.0,
+ )
diff --git a/tests/eko/kernels/test_kernels_QEDsinglet.py b/tests/eko/kernels/test_kernels_QEDsinglet.py
new file mode 100644
index 000000000..f9729dfbe
--- /dev/null
+++ b/tests/eko/kernels/test_kernels_QEDsinglet.py
@@ -0,0 +1,131 @@
+import warnings
+
+import numpy as np
+import pytest
+
+from eko.kernels import singlet_qed as s
+from ekore.anomalous_dimensions.unpolarized import space_like as ad
+
+methods = [
+ # "iterate-expanded",
+ # "decompose-expanded",
+ # "perturbative-expanded",
+ # "truncated",
+ # "ordered-truncated",
+ "iterate-exact",
+ # "decompose-exact",
+ # "perturbative-exact",
+]
+
+
+def test_zero(monkeypatch):
+ """No evolution results in exp(0)"""
+ nf = 3
+ ev_op_iterations = 2
+ ev_op_max_order = (3, 0)
+ for qcd in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ order = (qcd, qed)
+ gamma_s = (
+ np.random.rand(qcd + 1, qed + 1, 4, 4)
+ + np.random.rand(qcd + 1, qed + 1, 4, 4) * 1j
+ )
+ # monkeypatch.setattr(
+ # ad,
+ # "exp_matrix_2D",
+ # lambda gamma_S: (
+ # gamma_S,
+ # 1,
+ # 1,
+ # np.array([[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]),
+ # np.array([[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]),
+ # np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]),
+ # np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 1]]),
+ # ),
+ # )
+ for method in methods:
+ np.testing.assert_allclose(
+ s.dispatcher(
+ order,
+ method,
+ gamma_s,
+ [1, 1, 1],
+ np.array([[1, 1], [1, 1]]),
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ ),
+ np.eye(4),
+ )
+ np.testing.assert_allclose(
+ s.dispatcher(
+ order,
+ method,
+ np.zeros((qcd + 1, qed + 1, 4, 4), dtype=complex),
+ [1, 1.5, 2],
+ np.array([[1, 1], [1, 1]]),
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ ),
+ np.eye(4),
+ )
+
+
+def test_zero_true_gamma(monkeypatch):
+ """No evolution results in exp(0)"""
+ nf = 3
+ ev_op_iterations = 2
+ ev_op_max_order = (3, 0)
+ for qcd in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ order = (qcd, qed)
+ n = np.random.rand()
+ gamma_s = ad.gamma_singlet_qed(order, n, nf)
+ # monkeypatch.setattr(
+ # ad,
+ # "exp_matrix_2D",
+ # lambda gamma_S: (
+ # gamma_S,
+ # 1,
+ # 1,
+ # np.array([[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]),
+ # np.array([[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]),
+ # np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]),
+ # np.array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 1]]),
+ # ),
+ # )
+ for method in methods:
+ np.testing.assert_allclose(
+ s.dispatcher(
+ order,
+ method,
+ gamma_s,
+ [1, 1, 1],
+ np.array([[1, 1], [1, 1]]),
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ ),
+ np.eye(4),
+ )
+ np.testing.assert_allclose(
+ s.dispatcher(
+ order,
+ method,
+ np.zeros((qcd + 1, qed + 1, 4, 4), dtype=complex),
+ [1.0, 1.5, 2.0],
+ np.array([[1.25, 1], [1.75, 1]]),
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ ),
+ np.eye(4),
+ )
+
+
+def test_error():
+ with pytest.raises(NotImplementedError):
+ s.dispatcher(
+ (3, 2), "AAA", np.random.rand(4, 3, 2, 2), [0.2, 0.1], [0.01], 3, 10, 10
+ )
diff --git a/tests/eko/kernels/test_kernels_QEDvalence.py b/tests/eko/kernels/test_kernels_QEDvalence.py
new file mode 100644
index 000000000..8cb1ecf91
--- /dev/null
+++ b/tests/eko/kernels/test_kernels_QEDvalence.py
@@ -0,0 +1,107 @@
+import warnings
+
+import numpy as np
+import pytest
+
+from eko.kernels import valence_qed as val
+from ekore import anomalous_dimensions
+
+methods = [
+ # "iterate-expanded",
+ # "decompose-expanded",
+ # "perturbative-expanded",
+ # "truncated",
+ # "ordered-truncated",
+ "iterate-exact",
+ # "decompose-exact",
+ # "perturbative-exact",
+]
+
+
+def test_zero(monkeypatch):
+ """No evolution results in exp(0)"""
+ nf = 3
+ ev_op_iterations = 2
+ ev_op_max_order = (3, 0)
+ for qcd in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ order = (qcd, qed)
+ gamma_v = (
+ np.random.rand(qcd + 1, qed + 1, 2, 2)
+ + np.random.rand(qcd + 1, qed + 1, 2, 2) * 1j
+ )
+ for method in methods:
+ np.testing.assert_allclose(
+ val.dispatcher(
+ order,
+ method,
+ gamma_v,
+ [1, 1, 1],
+ np.array([[1, 1], [1, 1]]),
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ ),
+ np.identity(2),
+ )
+ np.testing.assert_allclose(
+ val.dispatcher(
+ order,
+ method,
+ np.zeros((qcd + 1, qed + 1, 2, 2), dtype=complex),
+ [1.0, 1.5, 2.0],
+ np.array([[1.25, 1], [1.75, 1]]),
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ ),
+ np.identity(2),
+ )
+
+
+def test_zero_true_gamma(monkeypatch):
+ """No evolution results in exp(0)"""
+ nf = 3
+ ev_op_iterations = 2
+ ev_op_max_order = (3, 0)
+ for qcd in range(1, 3 + 1):
+ for qed in range(1, 2 + 1):
+ order = (qcd, qed)
+ n = np.random.rand()
+ gamma_v = anomalous_dimensions.unpolarized.space_like.gamma_valence_qed(
+ order, n, nf
+ )
+ for method in methods:
+ np.testing.assert_allclose(
+ val.dispatcher(
+ order,
+ method,
+ gamma_v,
+ [1, 1, 1],
+ np.array([[1, 1], [1, 1]]),
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ ),
+ np.identity(2),
+ )
+ np.testing.assert_allclose(
+ val.dispatcher(
+ order,
+ method,
+ np.zeros((qcd + 1, qed + 1, 2, 2), dtype=complex),
+ [1.0, 1.5, 2.0],
+ np.array([[1.25, 1], [1.75, 1]]),
+ nf,
+ ev_op_iterations,
+ ev_op_max_order,
+ ),
+ np.identity(2),
+ )
+
+
+def test_error():
+ with pytest.raises(NotImplementedError):
+ val.dispatcher(
+ (3, 2), "AAA", np.random.rand(4, 3, 2, 2), [0.2, 0.1], [0.01], 3, 10, 10
+ )
diff --git a/tests/eko/kernels/test_ns.py b/tests/eko/kernels/test_ns.py
index 9e89492aa..7b3c16a3c 100644
--- a/tests/eko/kernels/test_ns.py
+++ b/tests/eko/kernels/test_ns.py
@@ -203,7 +203,7 @@ def test_ode_n3lo():
def test_error():
- with pytest.raises(NotImplementedError):
+ with pytest.raises(ValueError):
ns.dispatcher((5, 0), "iterate-exact", np.random.rand(3) + 0j, 0.2, 0.1, 3, 10)
with pytest.raises(NotImplementedError):
ad.gamma_ns((2, 0), 10202, 1, 3)
diff --git a/tests/eko/kernels/test_s.py b/tests/eko/kernels/test_s.py
index 618699e84..5380ba319 100644
--- a/tests/eko/kernels/test_s.py
+++ b/tests/eko/kernels/test_s.py
@@ -3,8 +3,8 @@
import numpy as np
import pytest
-from ekore import anomalous_dimensions as ad
from eko.kernels import singlet as s
+from ekore import anomalous_dimensions as ad
methods = [
"iterate-expanded",
@@ -26,7 +26,7 @@ def test_zero_lo(monkeypatch):
gamma_s = np.random.rand(1, 2, 2) + np.random.rand(1, 2, 2) * 1j
monkeypatch.setattr(
ad,
- "exp_singlet",
+ "exp_matrix_2D",
lambda gamma_S: (
gamma_S,
1,
@@ -65,7 +65,7 @@ def test_zero_nlo_decompose(monkeypatch):
gamma_s = np.random.rand(2, 2, 2) + np.random.rand(2, 2, 2) * 1j
monkeypatch.setattr(
ad,
- "exp_singlet",
+ "exp_matrix_2D",
lambda gamma_S: (
gamma_S,
1,
@@ -107,7 +107,7 @@ def test_zero_nnlo_decompose(monkeypatch):
gamma_s = np.random.rand(3, 2, 2) + np.random.rand(3, 2, 2) * 1j
monkeypatch.setattr(
ad,
- "exp_singlet",
+ "exp_matrix_2D",
lambda gamma_S: (
gamma_S,
1,
@@ -149,7 +149,7 @@ def test_zero_n3lo_decompose(monkeypatch):
gamma_s = np.random.rand(4, 2, 2) + np.random.rand(4, 2, 2) * 1j
monkeypatch.setattr(
ad,
- "exp_singlet",
+ "exp_matrix_2D",
lambda gamma_S: (
gamma_S,
1,
diff --git a/tests/eko/scale_variations/test_expanded.py b/tests/eko/scale_variations/test_expanded.py
index c0f6d4c2b..b58a785b1 100644
--- a/tests/eko/scale_variations/test_expanded.py
+++ b/tests/eko/scale_variations/test_expanded.py
@@ -28,6 +28,23 @@ def test_ns_sv_dispacher():
)
+def test_ns_sv_dispacher_qed():
+ """Test to identity"""
+ order = (4, 2)
+ gamma_ns = np.random.rand(order[0], order[1])
+ L = 0
+ nf = 5
+ a_s = 0.35
+ a_em = 0.01
+ for alphaem_running in [True, False]:
+ np.testing.assert_allclose(
+ expanded.non_singlet_variation_qed(
+ gamma_ns, a_s, a_em, alphaem_running, order, nf, L
+ ),
+ 1.0,
+ )
+
+
def test_singlet_sv_dispacher():
"""Test to identity"""
order = (4, 0)
@@ -36,10 +53,44 @@ def test_singlet_sv_dispacher():
nf = 5
a_s = 0.35
np.testing.assert_allclose(
- expanded.singlet_variation(gamma_singlet, a_s, order, nf, L), np.eye(2)
+ expanded.singlet_variation(gamma_singlet, a_s, order, nf, L, 2), np.eye(2)
)
+def test_singlet_sv_dispacher_qed():
+ """Test to identity"""
+ order = (4, 2)
+ gamma_singlet = np.random.rand(order[0], order[1], 4, 4)
+ L = 0
+ nf = 5
+ a_s = 0.35
+ a_em = 0.01
+ for alphaem_running in [True, False]:
+ np.testing.assert_allclose(
+ expanded.singlet_variation_qed(
+ gamma_singlet, a_s, a_em, alphaem_running, order, nf, L
+ ),
+ np.eye(4),
+ )
+
+
+def test_valence_sv_dispacher_qed():
+ """Test to identity"""
+ order = (4, 2)
+ gamma_valence = np.random.rand(order[0], order[1], 2, 2)
+ L = 0
+ nf = 5
+ a_s = 0.35
+ a_em = 0.01
+ for alphaem_running in [True, False]:
+ np.testing.assert_allclose(
+ expanded.valence_variation_qed(
+ gamma_valence, a_s, a_em, alphaem_running, order, nf, L
+ ),
+ np.eye(2),
+ )
+
+
def test_scale_variation_a_vs_b():
r"""
Test ``ModSV=exponentiated`` kernel vs ``ModSV=expanded``.
@@ -133,7 +184,7 @@ def scheme_diff(g, k, pto, is_singlet):
ker_a = singlet.dispatcher(
order, method, gs_a, a1, a0, nf, ev_op_iterations=1, ev_op_max_order=1
)
- ker_b = ker @ expanded.singlet_variation(gs, a1, order, nf, L)
+ ker_b = ker @ expanded.singlet_variation(gs, a1, order, nf, L, 2)
s_diff = scheme_diff(gs, L, order, True)
np.testing.assert_allclose(
(ker_a - ker_b) @ np.linalg.inv(ker),
diff --git a/tests/eko/scale_variations/test_exponentiated.py b/tests/eko/scale_variations/test_exponentiated.py
index 1d0fad4eb..dd7053c9f 100644
--- a/tests/eko/scale_variations/test_exponentiated.py
+++ b/tests/eko/scale_variations/test_exponentiated.py
@@ -1,6 +1,6 @@
import numpy as np
-from eko.scale_variations.exponentiated import gamma_variation
+from eko.scale_variations.exponentiated import gamma_variation, gamma_variation_qed
def test_gamma_ns_fact():
@@ -29,3 +29,23 @@ def test_gamma_singlet_fact():
assert gamma_s_NNLO_1[2] - gamma_s[2] == 8.0
gamma_s_N3LO_0 = gamma_variation(gamma_s.copy(), (4, 0), 3, 0)
assert gamma_s_N3LO_0[3] == gamma_s[3]
+
+
+def test_gamma_ns_qed_fact():
+ gamma_ns = np.array(
+ [
+ [0.0, 1.0, 0.5, 0.25, 0.125],
+ [0.2, 0.1, 0.0, 0.0, 0.0],
+ [0.15, 0.0, 0.0, 0.0, 0.0],
+ ]
+ ).transpose()
+ gamma_ns_as1aem1_0 = gamma_variation_qed(gamma_ns.copy(), (1, 1), 3, 0, True)
+ np.testing.assert_allclose(gamma_ns_as1aem1_0, gamma_ns)
+ gamma_ns_as1aem1_1 = gamma_variation_qed(gamma_ns.copy(), (1, 1), 3, 1, True)
+ np.testing.assert_allclose(gamma_ns_as1aem1_1, gamma_ns)
+ gamma_ns_as2aem2_1 = gamma_variation_qed(gamma_ns.copy(), (2, 2), 3, 1, True)
+ assert gamma_ns_as2aem2_1[2, 0] < gamma_ns[2, 0]
+ assert gamma_ns_as2aem2_1[0, 2] > gamma_ns[0, 2] # beta0qed < 0
+ gamma_ns_as4aem2_1 = gamma_variation_qed(gamma_ns.copy(), (4, 2), 3, 1, True)
+ gamma_ns_N3LO_1 = gamma_variation(gamma_ns.copy()[1:, 0], (4, 0), 3, 1)
+ np.testing.assert_allclose(gamma_ns_as4aem2_1[1:, 0], gamma_ns_N3LO_1)
diff --git a/tests/eko/test_basis_rotation.py b/tests/eko/test_basis_rotation.py
index fdd6b4de1..680ef065d 100644
--- a/tests/eko/test_basis_rotation.py
+++ b/tests/eko/test_basis_rotation.py
@@ -28,6 +28,48 @@ def test_ad_projector():
np.testing.assert_allclose(v3 @ ns_m, v3)
+def test_ad_projector_qed():
+ s = br.rotate_flavor_to_unified_evolution[2]
+ g = br.rotate_flavor_to_unified_evolution[0]
+ vd3 = br.rotate_flavor_to_unified_evolution[br.unified_evol_basis.index("Vd3")]
+ vd8 = br.rotate_flavor_to_unified_evolution[br.unified_evol_basis.index("Vd8")]
+ vu3 = br.rotate_flavor_to_unified_evolution[br.unified_evol_basis.index("Vu3")]
+ vu8 = br.rotate_flavor_to_unified_evolution[br.unified_evol_basis.index("Vu8")]
+
+ s_to_s = br.ad_projector((100, 100), nf=6, qed=True)
+ np.testing.assert_allclose(s @ s_to_s, s)
+ np.testing.assert_allclose(g @ s_to_s, 0.0)
+ np.testing.assert_allclose(vd3 @ s_to_s, 0.0)
+
+ g_to_s = br.ad_projector((21, 100), nf=6, qed=True)
+
+ np.testing.assert_allclose(s @ g_to_s, 0.0)
+ np.testing.assert_allclose(g @ g_to_s, s)
+ np.testing.assert_allclose(vd3 @ g_to_s, 0.0)
+
+ ns_md = br.ad_projector((br.non_singlet_pids_map["ns-d"], 0), nf=6, qed=True)
+
+ np.testing.assert_allclose(s @ ns_md, 0.0, atol=1e-15)
+ np.testing.assert_allclose(g @ ns_md, 0.0)
+ np.testing.assert_allclose(vd3 @ ns_md, vd3)
+ np.testing.assert_allclose(vd8 @ ns_md, vd8)
+
+ ns_md = br.ad_projector((br.non_singlet_pids_map["ns-d"], 0), nf=3, qed=True)
+ ns_mu = br.ad_projector((br.non_singlet_pids_map["ns-u"], 0), nf=3, qed=True)
+ np.testing.assert_allclose(vd3 @ ns_md, vd3)
+ np.testing.assert_allclose(vd8 @ ns_md, 0.0)
+ np.testing.assert_allclose(vu3 @ ns_mu, 0.0)
+ np.testing.assert_allclose(vu8 @ ns_mu, 0.0)
+
+ ns_mu = br.ad_projector((br.non_singlet_pids_map["ns-u"], 0), nf=4, qed=True)
+ np.testing.assert_allclose(vu3 @ ns_mu, vu3)
+ np.testing.assert_allclose(vu8 @ ns_mu, 0.0)
+
+ ns_mu = br.ad_projector((br.non_singlet_pids_map["ns-u"], 0), nf=6, qed=True)
+ np.testing.assert_allclose(vu3 @ ns_mu, vu3)
+ np.testing.assert_allclose(vu8 @ ns_mu, vu8)
+
+
def test_ad_projectors():
for nf in range(3, 6 + 1):
diag = np.array([0] * (1 + 6 - nf) + [1] * (1 + 2 * nf) + [0] * (6 - nf))
diff --git a/tests/eko/test_beta.py b/tests/eko/test_beta.py
index fb089bb62..411ff0bda 100644
--- a/tests/eko/test_beta.py
+++ b/tests/eko/test_beta.py
@@ -26,9 +26,9 @@ def test_beta_as2():
def test_beta_aem2():
"""Test first beta function coefficient"""
- # from hep-ph/9706430
+ # from hep-ph/9803211
np.testing.assert_approx_equal(
- beta.beta_qed_aem2(5), -4.0 / 3 * 3 * (2 * 4 / 9 + 3 * 1 / 9)
+ beta.beta_qed_aem2(5), -4.0 / 3 * (3 + 3 * (2 * 4 / 9 + 3 * 1 / 9))
)
@@ -41,9 +41,9 @@ def test_beta_as3():
def test_beta_aem3():
"""Test second beta function coefficient"""
- # from hep-ph/9706430
+ # from hep-ph/9803211
np.testing.assert_approx_equal(
- beta.beta_qed_aem3(5), -4.0 * 3 * (2 * 16 / 81 + 3 * 1 / 81)
+ beta.beta_qed_aem3(5), -4.0 * (3 + 3 * (2 * 16 / 81 + 3 * 1 / 81))
)
diff --git a/tests/eko/test_couplings.py b/tests/eko/test_couplings.py
index b2282a670..01ece7682 100644
--- a/tests/eko/test_couplings.py
+++ b/tests/eko/test_couplings.py
@@ -90,6 +90,15 @@ def test_init(self):
hqm_scheme=QuarkMassSchemes.POLE,
thresholds_ratios=[1.0, 1.0, 1.0],
)
+ with pytest.raises(NotImplementedError):
+ Couplings(
+ couplings,
+ (0, 2),
+ evmod,
+ masses,
+ hqm_scheme=QuarkMassSchemes.POLE,
+ thresholds_ratios=MatchingScales(c=1.0, b=1.0, t=1.0),
+ )
with pytest.raises(ValueError):
coup2 = copy.deepcopy(couplings)
coup2.alphaem.value = 0
@@ -158,7 +167,7 @@ def test_ref(self):
)
)
for thresh_setup in thresh_setups:
- for order_s in [0, 1, 2, 3, 4]:
+ for order_s in [1, 2, 3, 4]:
for order_em in [0, 1, 2]:
for evmod in CouplingEvolutionMethod:
# if order_em == 1 and method == "expanded" and order_s != 0:
@@ -215,50 +224,66 @@ def test_exact(self):
]
alpharef = np.array([0.118, 0.00781])
muref = 91.0
- couplings = CouplingsRef.from_dict(
- dict(
- alphas=[alpharef[0], muref],
- alphaem=[alpharef[1], nan],
- num_flavs_ref=None,
- max_num_flavs=6,
- )
- )
for thresh_setup in thresh_setups:
for qcd in range(1, 4 + 1):
for qed in range(2 + 1):
- pto = (qcd, qed)
- sc_expanded = Couplings(
- couplings,
- pto,
- method=CouplingEvolutionMethod.EXPANDED,
- masses=masses,
- hqm_scheme=QuarkMassSchemes.POLE,
- thresholds_ratios=thresh_setup,
- )
- sc_exact = Couplings(
- couplings,
- pto,
- method=CouplingEvolutionMethod.EXACT,
- masses=masses,
- hqm_scheme=QuarkMassSchemes.POLE,
- thresholds_ratios=thresh_setup,
- )
- if pto in [(1, 0), (1, 1), (1, 2)]:
- precisions = (5e-4, 5e-5)
- else:
- precisions = (5e-3, 5e-4)
- for q2 in [1, 1e1, 1e2, 1e3, 1e4]:
- # At LO (either QCD or QED LO) the exact and expanded
- # solutions coincide, while beyond LO they don't.
- # Therefore if the path is too long they start being different.
- if q2 in [1, 1e1] and pto not in [(1, 0), (0, 1)]:
- continue
- np.testing.assert_allclose(
- sc_expanded.a(q2)[0], sc_exact.a(q2)[0], rtol=precisions[0]
+ for qedref in [muref, nan]: # testing both running and fixed alpha
+ pto = (qcd, qed)
+ couplings = CouplingsRef.from_dict(
+ dict(
+ alphas=[alpharef[0], muref],
+ alphaem=[alpharef[1], qedref],
+ num_flavs_ref=None,
+ max_num_flavs=6,
+ )
)
- np.testing.assert_allclose(
- sc_expanded.a(q2)[1], sc_exact.a(q2)[1], rtol=precisions[1]
+ sc_expanded = Couplings(
+ couplings,
+ pto,
+ method=CouplingEvolutionMethod.EXPANDED,
+ masses=masses,
+ hqm_scheme=QuarkMassSchemes.POLE,
+ thresholds_ratios=thresh_setup,
+ )
+ sc_exact = Couplings(
+ couplings,
+ pto,
+ method=CouplingEvolutionMethod.EXACT,
+ masses=masses,
+ hqm_scheme=QuarkMassSchemes.POLE,
+ thresholds_ratios=thresh_setup,
)
+ if pto in [(1, 0), (1, 1), (1, 2)]:
+ precisions = (5e-4, 5e-4)
+ else:
+ precisions = (5e-3, 5e-4)
+ for q2 in [1, 1e1, 1e2, 1e3, 1e4]:
+ # At LO (either QCD or QED LO) the exact and expanded
+ # solutions coincide, while beyond LO they don't.
+ # Therefore if the path is too long they start being different.
+ if q2 in [1, 1e1] and pto not in [(1, 0)]:
+ continue
+ np.testing.assert_allclose(
+ sc_expanded.a(q2)[0],
+ sc_exact.a(q2)[0],
+ rtol=precisions[0],
+ )
+ np.testing.assert_allclose(
+ sc_expanded.a(q2)[1],
+ sc_exact.a(q2)[1],
+ rtol=precisions[1],
+ )
+ if qedref is nan or qed == 0:
+ np.testing.assert_allclose(
+ sc_expanded.a(q2)[1],
+ alpharef[1] / (4 * np.pi),
+ rtol=1e-10,
+ )
+ np.testing.assert_allclose(
+ sc_exact.a(q2)[1],
+ alpharef[1] / (4 * np.pi),
+ rtol=1e-10,
+ )
def benchmark_expanded_n3lo(self):
"""test N3LO - NNLO expansion with some reference value from Mathematica"""
diff --git a/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_aem1.py b/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_aem1.py
index 0cf4d26e1..d114ff5df 100644
--- a/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_aem1.py
+++ b/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_aem1.py
@@ -1,23 +1,24 @@
# Test LO splitting functions
import numpy as np
-import ekore.harmonics as h
import ekore.anomalous_dimensions.unpolarized.space_like as ad_us
+import ekore.harmonics as h
+from eko import constants
def test_number_conservation():
# number
N = complex(1.0, 0.0)
- s1 = h.S1(N)
- np.testing.assert_almost_equal(ad_us.aem1.gamma_ns(N, s1), 0)
+ sx = h.sx(N, max_weight=1)
+ np.testing.assert_almost_equal(ad_us.aem1.gamma_ns(N, sx), 0)
def test_quark_momentum_conservation():
# quark momentum
N = complex(2.0, 0.0)
- s1 = h.S1(N)
+ sx = h.sx(N, max_weight=1)
np.testing.assert_almost_equal(
- ad_us.aem1.gamma_ns(N, s1) + ad_us.aem1.gamma_phq(N),
+ ad_us.aem1.gamma_ns(N, sx) + ad_us.aem1.gamma_phq(N),
0,
)
@@ -26,6 +27,11 @@ def test_photon_momentum_conservation():
# photon momentum
N = complex(2.0, 0.0)
for NF in range(2, 6 + 1):
+ NU = constants.uplike_flavors(NF)
+ ND = NF - NU
np.testing.assert_almost_equal(
- ad_us.aem1.gamma_qph(N, NF) + ad_us.aem1.gamma_phph(NF), 0
+ constants.eu2 * ad_us.aem1.gamma_qph(N, NU)
+ + constants.ed2 * ad_us.aem1.gamma_qph(N, ND)
+ + ad_us.aem1.gamma_phph(NF),
+ 0,
)
diff --git a/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_aem2.py b/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_aem2.py
index 0a1ddabde..8845e8bb6 100644
--- a/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_aem2.py
+++ b/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_aem2.py
@@ -10,9 +10,14 @@ def test_number_conservation():
# number
N = complex(1.0, 0.0)
sx = h.sx(N, 3)
+ sx_ns_qed = h.compute_qed_ns_cache(N, sx[0])
for NF in range(2, 6 + 1):
- np.testing.assert_almost_equal(ad.aem2.gamma_nsmu(N, NF, sx), 0, decimal=4)
- np.testing.assert_almost_equal(ad.aem2.gamma_nsmd(N, NF, sx), 0, decimal=4)
+ np.testing.assert_almost_equal(
+ ad.aem2.gamma_nsmu(N, NF, sx, sx_ns_qed), 0, decimal=4
+ )
+ np.testing.assert_almost_equal(
+ ad.aem2.gamma_nsmd(N, NF, sx, sx_ns_qed), 0, decimal=4
+ )
def test_photon_momentum_conservation():
@@ -34,11 +39,12 @@ def test_quark_momentum_conservation():
# quark momentum
N = complex(2.0, 0.0)
sx = h.sx(N, 3)
+ sx_ns_qed = h.compute_qed_ns_cache(N, sx[0])
NF = 6
NU = constants.uplike_flavors(NF)
ND = NF - NU
np.testing.assert_almost_equal(
- ad.aem2.gamma_nspu(N, NF, sx)
+ ad.aem2.gamma_nspu(N, NF, sx, sx_ns_qed)
+ constants.eu2 * ad.aem2.gamma_ps(N, NU)
+ constants.ed2 * ad.aem2.gamma_ps(N, ND)
+ ad.aem2.gamma_phu(N, NF, sx),
@@ -46,7 +52,7 @@ def test_quark_momentum_conservation():
decimal=4,
)
np.testing.assert_almost_equal(
- ad.aem2.gamma_nspd(N, NF, sx)
+ ad.aem2.gamma_nspd(N, NF, sx, sx_ns_qed)
+ constants.eu2 * ad.aem2.gamma_ps(N, NU)
+ constants.ed2 * ad.aem2.gamma_ps(N, ND)
+ ad.aem2.gamma_phd(N, NF, sx),
diff --git a/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_as1aem1.py b/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_as1aem1.py
index ce77d4f3a..525ba24a9 100644
--- a/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_as1aem1.py
+++ b/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_as1aem1.py
@@ -11,7 +11,8 @@ def test_number_conservation():
# number
N = complex(1.0, 0.0)
sx = h.sx(N, 3)
- np.testing.assert_almost_equal(ad.as1aem1.gamma_nsm(N, sx), 0, decimal=4)
+ sx_ns_qed = h.compute_qed_ns_cache(N, sx[0])
+ np.testing.assert_almost_equal(ad.as1aem1.gamma_nsm(N, sx, sx_ns_qed), 0, decimal=4)
def test_gluon_momentum_conservation():
@@ -52,8 +53,9 @@ def test_quark_momentum_conservation():
# quark momentum
N = complex(2.0, 0.0)
sx = h.sx(N, 3)
+ sx_ns_qed = h.compute_qed_ns_cache(N, sx[0])
np.testing.assert_almost_equal(
- ad.as1aem1.gamma_nsp(N, sx)
+ ad.as1aem1.gamma_nsp(N, sx, sx_ns_qed)
+ ad.as1aem1.gamma_gq(N, sx)
+ ad.as1aem1.gamma_phq(N, sx),
0,
diff --git a/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_init.py b/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_init.py
index f36efec04..b83d80fa8 100644
--- a/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_init.py
+++ b/tests/ekore/anomalous_dimensions/unpolarized/space_like/test_init.py
@@ -2,12 +2,13 @@
import warnings
import numpy as np
+import pytest
from numpy.testing import assert_allclose, assert_almost_equal, assert_raises
+from scipy.linalg import expm
-from ekore import anomalous_dimensions as ad
-from eko import basis_rotation as br
import ekore.anomalous_dimensions.unpolarized.space_like as ad_us
-from ekore.anomalous_dimensions.unpolarized.space_like import as1 as ad_as1
+from eko import basis_rotation as br
+from ekore import anomalous_dimensions as ad
from ekore import harmonics
NF = 5
@@ -16,8 +17,8 @@
def test_eigensystem_gamma_singlet_0_values():
n = 3
s1 = harmonics.S1(n)
- gamma_S_0 = ad_as1.gamma_singlet(3, s1, NF)
- res = ad.exp_singlet(gamma_S_0)
+ gamma_S_0 = ad_us.as1.gamma_singlet(3, s1, NF)
+ res = ad.exp_matrix_2D(gamma_S_0)
lambda_p = complex(12.273612971466964, 0)
lambda_m = complex(5.015275917421917, 0)
e_p = np.array(
@@ -35,6 +36,32 @@ def test_eigensystem_gamma_singlet_0_values():
assert_allclose(e_m, res[4])
+def test_exp_matrix():
+ n = 3
+ s1 = harmonics.S1(n)
+ gamma_S_0 = ad_us.as1.gamma_singlet(3, s1, NF)
+ res = ad.exp_matrix_2D(gamma_S_0)[0]
+ res2 = ad.exp_matrix(gamma_S_0)[0]
+ assert_allclose(res, res2)
+ gamma_S_0_qed = ad_us.as1.gamma_singlet_qed(3, s1, NF)
+ res = expm(gamma_S_0_qed)
+ res2 = ad.exp_matrix(gamma_S_0_qed)[0]
+ assert_allclose(res, res2)
+ gamma_2D = np.random.rand(2, 2) + np.random.rand(2, 2) * 1j
+ res1 = expm(gamma_2D)
+ res2 = ad.exp_matrix(gamma_2D)[0]
+ assert_almost_equal(res1, res2)
+ diag = np.diag([1, 2, 3, 4])
+ assert_allclose(np.diag(np.exp([1, 2, 3, 4])), ad.exp_matrix(diag)[0])
+ id_ = np.identity(4, np.complex_)
+ zero = np.zeros((4, 4), np.complex_)
+ assert_allclose(id_, ad.exp_matrix(zero)[0])
+ sigma2 = np.array([[0.0, -1.0j], [1.0j, 0.0]])
+ exp = ad.exp_matrix(sigma2)[0]
+ exp_m = ad.exp_matrix(-sigma2)[0]
+ assert_almost_equal(np.identity(2, np.complex_), exp @ exp_m)
+
+
def test_eigensystem_gamma_singlet_projectors_EV():
nf = 3
for N in [3, 4]: # N=2 seems close to 0, so test fails
@@ -43,7 +70,7 @@ def test_eigensystem_gamma_singlet_projectors_EV():
# ignore Runtime Warnings
warnings.simplefilter("ignore", RuntimeWarning)
for gamma_S in ad_us.gamma_singlet(o, N, nf):
- _exp, l_p, l_m, e_p, e_m = ad.exp_singlet(gamma_S)
+ _exp, l_p, l_m, e_p, e_m = ad.exp_matrix_2D(gamma_S)
# projectors behave as P_a . P_b = delta_ab P_a
assert_allclose(np.dot(e_p, e_p), e_p)
assert_almost_equal(np.dot(e_p, e_m), np.zeros((2, 2)))
@@ -55,17 +82,17 @@ def test_eigensystem_gamma_singlet_projectors_EV():
def test_gamma_ns():
nf = 3
- # LO
+ # ad_us.as1
assert_almost_equal(
ad_us.gamma_ns((3, 0), br.non_singlet_pids_map["ns+"], 1, nf)[0], 0.0
)
- # NLO
+ # ad_us.as2
assert_allclose(
ad_us.gamma_ns((2, 0), br.non_singlet_pids_map["ns-"], 1, nf),
np.zeros(2),
atol=2e-6,
)
- # NNLO
+ # ad_us.as3
assert_allclose(
ad_us.gamma_ns((3, 0), br.non_singlet_pids_map["ns-"], 1, nf),
np.zeros(3),
@@ -76,7 +103,7 @@ def test_gamma_ns():
np.zeros(3),
atol=8e-4,
)
- # N3LO
+ # as4
assert_allclose(
ad_us.gamma_ns((4, 0), br.non_singlet_pids_map["ns-"], 1, nf),
np.zeros(4),
@@ -94,3 +121,112 @@ def test_gamma_ns():
ad_us.gamma_ns((4, 0), br.non_singlet_pids_map["ns+"], 1, nf),
np.zeros(4),
)
+
+
+def test_gamma_ns_qed():
+ nf = 3
+ # aem1
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((1, 1), br.non_singlet_pids_map["ns-u"], 1, nf),
+ np.zeros((2, 2)),
+ decimal=5,
+ )
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((1, 1), br.non_singlet_pids_map["ns-d"], 1, nf),
+ np.zeros((2, 2)),
+ decimal=5,
+ )
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((1, 1), br.non_singlet_pids_map["ns+u"], 1, nf)[0, 1],
+ 0,
+ decimal=5,
+ )
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((1, 1), br.non_singlet_pids_map["ns+d"], 1, nf)[0, 1],
+ 0,
+ decimal=5,
+ )
+ # as1aem1
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((1, 2), br.non_singlet_pids_map["ns-u"], 1, nf),
+ np.zeros((2, 3)),
+ decimal=5,
+ )
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((1, 2), br.non_singlet_pids_map["ns-d"], 1, nf),
+ np.zeros((2, 3)),
+ decimal=5,
+ )
+ # aem2
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((1, 2), br.non_singlet_pids_map["ns-u"], 1, nf),
+ np.zeros((2, 3)),
+ decimal=5,
+ )
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((1, 2), br.non_singlet_pids_map["ns-d"], 1, nf),
+ np.zeros((2, 3)),
+ decimal=5,
+ )
+ # ad_us.as2
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((2, 1), br.non_singlet_pids_map["ns-u"], 1, nf),
+ np.zeros((3, 2)),
+ decimal=5,
+ )
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((2, 1), br.non_singlet_pids_map["ns-d"], 1, nf),
+ np.zeros((3, 2)),
+ decimal=5,
+ )
+ # ad_us.as3
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((3, 1), br.non_singlet_pids_map["ns-u"], 1, nf),
+ np.zeros((4, 2)),
+ decimal=3,
+ )
+ assert_almost_equal(
+ ad_us.gamma_ns_qed((3, 1), br.non_singlet_pids_map["ns-d"], 1, nf),
+ np.zeros((4, 2)),
+ decimal=3,
+ )
+
+
+def test_dim_singlet():
+ nf = 3
+ N = 2
+ sx = harmonics.sx(N, max_weight=3 + 1)
+ gamma_singlet = ad_us.gamma_singlet_qed((3, 2), N, nf)
+ assert gamma_singlet.shape == (4, 3, 4, 4)
+ gamma_singlet_as1 = ad_us.as1.gamma_singlet_qed(N, sx[0], nf)
+ assert gamma_singlet_as1.shape == (4, 4)
+ gamma_singlet_as2 = ad_us.as2.gamma_singlet_qed(N, nf, sx)
+ assert gamma_singlet_as2.shape == (4, 4)
+ gamma_singlet_as3 = ad_us.as3.gamma_singlet_qed(N, nf, sx)
+ assert gamma_singlet_as3.shape == (4, 4)
+
+
+def test_dim_valence():
+ nf = 3
+ N = 2
+ sx = harmonics.sx(N, max_weight=3 + 1)
+ gamma_valence = ad_us.gamma_valence_qed((3, 2), N, nf)
+ assert gamma_valence.shape == (4, 3, 2, 2)
+ gamma_valence_as1 = ad_us.as1.gamma_valence_qed(N, sx[0])
+ assert gamma_valence_as1.shape == (2, 2)
+ gamma_valence_as2 = ad_us.as2.gamma_valence_qed(N, nf, sx)
+ assert gamma_valence_as2.shape == (2, 2)
+ gamma_valence_as3 = ad_us.as3.gamma_valence_qed(N, nf, sx)
+ assert gamma_valence_as3.shape == (2, 2)
+
+
+def test_dim_nsp():
+ nf = 3
+ N = 2
+ sx = harmonics.sx(N, max_weight=3 + 1)
+ gamma_nsup = ad_us.gamma_ns_qed((3, 2), 10102, N, nf)
+ assert gamma_nsup.shape == (4, 3)
+ gamma_nsdp = ad_us.gamma_ns_qed((3, 2), 10103, N, nf)
+ assert gamma_nsdp.shape == (4, 3)
+ with pytest.raises(NotImplementedError):
+ gamma_ns = ad_us.gamma_ns_qed((2, 0), 10106, N, nf)