Skip to content

Commit 4e5ed37

Browse files
Merge pull request #2344 from NNPDF/matching-evolution-grid
New evolution grid
2 parents 5af0a17 + ab21423 commit 4e5ed37

File tree

4 files changed

+148
-150
lines changed

4 files changed

+148
-150
lines changed

n3fit/src/evolven3fit/eko_utils.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,18 @@ def _eko_theory_from_nnpdf_theory(nnpdf_theory):
3636

3737
def construct_eko_cards(
3838
nnpdf_theory,
39-
q_fin,
40-
q_points,
4139
x_grid,
4240
op_card_dict: Optional[Dict[str, Any]] = None,
4341
theory_card_dict: Optional[Dict[str, Any]] = None,
4442
legacy40: bool = False,
4543
):
4644
"""
4745
Return the theory and operator cards used to construct the eko.
48-
nnpdf_theory is a NNPDF theory card for which we are computing the operator card and eko
49-
q_fin is the final point of the q grid while q_points is the number of points of the grid.
46+
nnpdf_theory is a NNPDF theory card for which we are computing the operator card.
5047
x_grid is the x grid to be used.
5148
op_card_dict and theory_card_dict are optional updates that can be provided respectively to the
5249
operator card and to the theory card.
50+
legacy40 is a flag that can be set if you want to use the old grid from NNPDF4.0
5351
"""
5452
theory, thresholds = load_theory(nnpdf_theory, theory_card_dict)
5553

@@ -62,17 +60,19 @@ def construct_eko_cards(
6260

6361
# construct mugrid
6462

65-
# Generate the q2grid, if q_fin and q_points are None, use `nf0` to select a default
63+
# Generate the q2grid
6664
q2_grid = utils.generate_q2grid(
67-
mu0,
68-
q_fin,
69-
q_points,
70-
{
71-
theory["mc"]: thresholds["c"],
72-
theory["mb"]: thresholds["b"],
73-
theory["mt"]: thresholds["t"],
65+
Q0=mu0,
66+
Qmin=1.0,
67+
Qmax=1e5,
68+
total_points=50,
69+
total_points_ic=6,
70+
match_dict={
71+
"mc": theory["mc"],
72+
"mb": theory["mb"],
73+
"kcThr": theory["kcThr"],
74+
"kbThr": theory["kbThr"],
7475
},
75-
theory["nf0"],
7676
legacy40=legacy40,
7777
)
7878

n3fit/src/evolven3fit/q2grids.py

Lines changed: 2 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
"""
2-
Definition of default Q2 grids
3-
4-
This file includes:
5-
6-
- ``Q2GRID_DEFAULT``: default NNPDF Q2 grid for evolution (55 points, starts at Q=1GeV)
7-
- ``Q2GRID_NNPDF40``: q2 grid used in the fits for the NNPDF4.0 release (49 points, starts at Q=1.65 GeV)
8-
- ``Q2GRID_Nf03``: q2 grid used in the perturvative charm fits for the NNPDF4.0 release (48 points, starts at Q=1GeV)
2+
Definition of LHAPDF evolution grid.
93
"""
104

115
import numpy as np
@@ -63,70 +57,5 @@
6357
6.0599320e04,
6458
1.0000000e05,
6559
]
66-
)
67-
** 2
60+
)**2
6861
)
69-
70-
Q2GRID_Nf03 = (
71-
np.array(
72-
[
73-
1.0000000e00,
74-
1.0768843e00,
75-
1.1642787e00,
76-
1.2640247e00,
77-
1.3783565e00,
78-
1.5100000e00,
79-
1.6573843e00,
80-
1.8279487e00,
81-
2.0263188e00,
82-
2.2582323e00,
83-
2.5308507e00,
84-
2.8531703e00,
85-
3.2365690e00,
86-
3.6955380e00,
87-
4.2486693e00,
88-
4.9200000e00,
89-
5.6571821e00,
90-
6.5475141e00,
91-
7.6300446e00,
92-
8.9555329e00,
93-
1.0590474e01,
94-
1.2622686e01,
95-
1.5169120e01,
96-
1.8386905e01,
97-
2.2489085e01,
98-
2.7767274e01,
99-
3.4624624e01,
100-
4.3624282e01,
101-
5.5561424e01,
102-
7.1571582e01,
103-
9.3295496e01,
104-
1.2313315e02,
105-
1.6464038e02,
106-
2.2315640e02,
107-
3.0681103e02,
108-
4.2816505e02,
109-
6.0692308e02,
110-
8.7449251e02,
111-
1.2817733e03,
112-
1.9127020e03,
113-
2.9082314e03,
114-
4.5095982e03,
115-
7.1379509e03,
116-
1.1543948e04,
117-
1.9094934e04,
118-
3.2338760e04,
119-
5.6137084e04,
120-
1.0000000e05,
121-
]
122-
)
123-
** 2
124-
)
125-
126-
Q2GRID_IC = (
127-
np.array([1.0000000e00, 1.0768843e00, 1.1642787e00, 1.2640247e00, 1.3783565e00, 1.5100000e00])
128-
** 2
129-
)
130-
131-
132-
Q2GRID_DEFAULT = np.concatenate([Q2GRID_IC, Q2GRID_NNPDF40])

n3fit/src/evolven3fit/utils.py

Lines changed: 93 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
from validphys.utils import yaml_safe
66

7-
from .q2grids import Q2GRID_DEFAULT, Q2GRID_NNPDF40
7+
from .q2grids import Q2GRID_NNPDF40
8+
9+
LAMBDA2 = 0.0625
810

911

1012
def read_runcard(usr_path):
@@ -19,43 +21,98 @@ def get_theoryID_from_runcard(usr_path):
1921
return my_runcard["theory"]["theoryid"]
2022

2123

22-
def generate_q2grid(Q0, Qfin, Q_points, match_dict, nf0=None, legacy40=False):
23-
"""Generate the q2grid used in the final evolved pdfs or use the default grid if Qfin or Q_points is
24-
not provided.
24+
def Q2_to_t(q2: float, lambda2) -> float:
25+
"""Map to define loglog spacing of the Q2grid"""
26+
return np.log(np.log(q2 / lambda2))
27+
28+
29+
def t_to_Q2(t: float, lambda2) -> float:
30+
"""Map to go from the loglog spacing back to Q2"""
31+
return lambda2 * np.exp(np.exp(t))
32+
33+
34+
def generate_q2grid(Q0, Qmin, Qmax, match_dict, total_points, total_points_ic, legacy40=False):
35+
"""Generate the q2grid used in the final evolved pdfs or use the default grid if legacy40 is set.
2536
26-
match_dict contains the couples (mass : factor) where factor is the number to be multiplied to mass
27-
in order to obtain the relative matching scale.
37+
The grid uses $\log(\log(Q^2/\text{`LAMBDA2`})) spacing between the points.
38+
39+
The grid from `Q2_min` --> `mc` is made separately from the rest to be able to let it contain at
40+
least 5 points. The reason for this is to always have some points in the intrinsic charm regime.
41+
The rest of the grids contains the same loglog spacing from threshold to threshold.
42+
43+
Since the grid needs to contain the $Q^2$ values `Q2_min`, `mc`^2, `Q0`^2, `mb`^2 and `Q2_max`,
44+
we divide the grid in batches that have these values as boundaries, and add them together
45+
at the end. The batches ad boundaries are thus like this:
46+
47+
Q2_min --> mc^2 --> Q0^2 --> mb^2 --> Q2_max
48+
49+
`match_dict` contains the quark mass thresholds and factors. This "factor" is the number to be
50+
multiplied to mass in order to obtain the relative matching scale (e.g. `match_dict["kbThr"]`
51+
in the case of the bottom threshold).
2852
"""
29-
if Qfin is None and Q_points is None:
30-
if legacy40:
31-
return Q2GRID_NNPDF40
32-
elif nf0 in (3, 4, 5):
33-
return Q2GRID_DEFAULT
34-
elif nf0 is None:
35-
raise ValueError("In order to use a default grid, a value of nf0 must be provided")
36-
else:
37-
raise NotImplementedError(f"No default grid in Q available for {nf0=}")
38-
elif Qfin is None or Q_points is None:
39-
raise ValueError("q_fin and q_points must be specified either both or none of them")
40-
else:
41-
grids = []
42-
Q_ini = Q0
43-
num_points_list = []
44-
for masses in match_dict:
45-
match_scale = masses * match_dict[masses]
46-
# Fraction of the total points to be included in this batch is proportional
47-
# to the log of the ratio between the initial scale and final scale of the
48-
# batch itself (normalized to the same log of the global initial and final
49-
# scales)
50-
if match_scale < Qfin:
51-
frac_of_point = np.log(match_scale / Q_ini) / np.log(Qfin / Q0)
52-
num_points = int(Q_points * frac_of_point)
53-
num_points_list.append(num_points)
54-
grids.append(np.geomspace(Q_ini**2, match_scale**2, num=num_points, endpoint=False))
55-
Q_ini = match_scale
56-
num_points = Q_points - sum(num_points_list)
57-
grids.append(np.geomspace(Q_ini**2, Qfin**2, num=num_points))
58-
return np.concatenate(grids).tolist()
53+
54+
# If flag --legacy40 is set return handmade legacy grid
55+
if legacy40:
56+
return Q2GRID_NNPDF40
57+
# Otherwise dynamically create the grid from Q2_min --> Q2_max
58+
59+
if Q0 < Qmin:
60+
raise ValueError("Q0 cannot be smaller than Qmin because the grid needs to contain Q0")
61+
62+
if Qmax < Qmin:
63+
raise ValueError("Qmax cannot be smaller than Qmin")
64+
65+
if total_points <= 5:
66+
raise ValueError("You need minimally 6 points (total_points > 5)")
67+
68+
if total_points_ic < 2 and Qmin < 1.502:
69+
raise ValueError("You need minimally 2 points below Q0 (total_points_ic > 1)")
70+
71+
Q2_min = Qmin**2 # 1.0**2
72+
Q2_max = Qmax**2 # 1e5**2
73+
74+
# Collect all node Q2's from Q0^2 --> Q2_max
75+
q0_2 = Q0**2
76+
node_Q2 = [q0_2, (match_dict["mb"] * match_dict["kbThr"]) ** 2, Q2_max]
77+
78+
# Make initial uniform grid in t from Q0^2 --> Q2_max
79+
t_min = Q2_to_t(q0_2, LAMBDA2)
80+
t_max = Q2_to_t(Q2_max, LAMBDA2)
81+
t_vals = np.linspace(t_min, t_max, total_points)
82+
q2_vals = t_to_Q2(t_vals, LAMBDA2)
83+
84+
# Count how many points fall into each subgrid
85+
n_intervals = len(node_Q2) - 1
86+
nQpoints = np.zeros(n_intervals, dtype=int)
87+
subgridindex = 0
88+
89+
for q2 in q2_vals:
90+
while subgridindex < n_intervals - 1 and q2 >= node_Q2[subgridindex + 1]:
91+
subgridindex += 1
92+
nQpoints[subgridindex] += 1
93+
94+
# Make t grid from Q2_min --> Q0^2
95+
t_min_ic = Q2_to_t(Q2_min, LAMBDA2)
96+
t_max_ic = Q2_to_t((match_dict["mc"] * match_dict["kcThr"]) ** 2, LAMBDA2)
97+
t_vals_ic = np.linspace(t_min_ic, t_max_ic, total_points_ic)
98+
q2_vals_ic = t_to_Q2(t_vals_ic, LAMBDA2)
99+
100+
# Now build each subgrid to contain the points we want
101+
grids = []
102+
grids.append(q2_vals_ic)
103+
for i in range(len(node_Q2) - 1):
104+
q2_lo, q2_hi = node_Q2[i], node_Q2[i + 1]
105+
t_lo, t_hi = Q2_to_t(q2_lo, LAMBDA2), Q2_to_t(q2_hi, LAMBDA2)
106+
npts = int(nQpoints[i])
107+
t_subgr = np.linspace(t_lo, t_hi, npts)
108+
q2_subgr = t_to_Q2(t_subgr, LAMBDA2)
109+
if i < n_intervals - 1:
110+
q2_subgr = q2_subgr[:-1]
111+
grids.append(q2_subgr)
112+
113+
# Combine all subgrids and return as an array
114+
q2_full = np.concatenate(grids)
115+
return q2_full
59116

60117

61118
def check_is_a_fit(config_folder):

n3fit/src/n3fit/tests/test_evolven3fit.py

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -73,27 +73,36 @@ def check_lhapdf_dat(dat_path, info):
7373

7474
def test_generate_q2grid():
7575
"""Tests the creation of the default grids is as expected"""
76-
# nf 3 or 4 q0 = 1.0
77-
grid = utils.generate_q2grid(None, None, None, {}, 3)
78-
assert grid[0] == 1.0**2
79-
grid = utils.generate_q2grid(None, None, None, {}, 4)
80-
assert grid[0] == 1.0**2
8176

82-
for nf in [1, 2, 6]:
83-
with pytest.raises(NotImplementedError):
84-
grid = utils.generate_q2grid(None, None, None, {}, nf)
77+
# Test if the correct errors are given
78+
with pytest.raises(TypeError):
79+
grid = utils.generate_q2grid(None, None, None, {}, None, None)
8580

8681
with pytest.raises(ValueError):
87-
grid = utils.generate_q2grid(None, None, None, {})
82+
grid = utils.generate_q2grid(
83+
1, 1, 1, {"mc": 1.502, "mb": 4.936, "kcThr": 1, "kbThr": 1}, 1, 1
84+
)
85+
86+
# Test if the grid contains the threshold values and the boundaries
87+
matched_grid = utils.generate_q2grid(
88+
1.65,
89+
1.0,
90+
100,
91+
{"mc": 1.502, "mb": 4.936, "kcThr": 1, "kbThr": 1},
92+
total_points=10,
93+
total_points_ic=3,
94+
)
95+
96+
for n in {1.502, 1.65, 4.936}:
97+
assert any(np.allclose(n**2, x) for x in matched_grid)
8898

89-
matched_grid = utils.generate_q2grid(1.65, 1.0e5, 100, {4.92: 2.0, 100: 1.0})
90-
t1 = 4.92 * 2.0
91-
t2 = 100.0 * 1.0
99+
np.testing.assert_allclose((1.0) ** 2, matched_grid[0])
100+
np.testing.assert_allclose((100.0) ** 2, matched_grid[-1])
92101

93-
np.testing.assert_allclose((1.65) ** 2, matched_grid[0])
94-
np.testing.assert_allclose((1.0e5) ** 2, matched_grid[-1])
95-
assert t1**2 in matched_grid
96-
assert t2**2 in matched_grid
102+
# Test the legacy40 grid
103+
legacy40 = utils.generate_q2grid(1, 1, 1, {}, 1, 1, legacy40=True)
104+
np.testing.assert_allclose((1.65) ** 2, legacy40[0])
105+
np.testing.assert_allclose((1e5) ** 2, legacy40[-1])
97106

98107

99108
def test_utils():
@@ -108,15 +117,13 @@ def test_utils():
108117

109118
def test_eko_utils(tmp_path, nnpdf_theory_card):
110119
# Testing construct eko cards
111-
q_fin = 100
112-
q_points = 5
120+
q_ini = 1.0
121+
q_fin = 1e5
113122
x_grid = [1.0e-3, 0.1, 1.0]
114123
pto = 2
115124
comments = "Test"
116125
t_card, op_card = eko_utils.construct_eko_cards(
117126
nnpdf_theory_card,
118-
q_fin,
119-
q_points,
120127
x_grid,
121128
op_card_dict={"configs": {"interpolation_polynomial_degree": 2}},
122129
theory_card_dict={"Comments": comments},
@@ -127,15 +134,20 @@ def test_eko_utils(tmp_path, nnpdf_theory_card):
127134
t_card_dict["order"][0] == pto + 1
128135
) # This is due to a different convention in eko orders due to QED
129136
np.testing.assert_allclose(op_card_dict["xgrid"], x_grid)
130-
# In theory 399 the charm threshold is at 1.51
131-
# and we should find two entries, one for nf=3 and another one for nf=4
132-
np.testing.assert_allclose(op_card_dict["mugrid"][0], (1.51, 3))
133-
np.testing.assert_allclose(op_card_dict["mugrid"][1], (1.51, 4))
134-
# Then (with the number of points we chosen it will happen in position 2,3
135-
# we will find the bottom threshold at two different nf
136-
np.testing.assert_allclose(op_card_dict["mugrid"][2], (4.92, 4))
137-
np.testing.assert_allclose(op_card_dict["mugrid"][3], (4.92, 5))
137+
# We should find two entries for each threshold energy,
138+
# one for nf=3(4) and another one for nf=4(5)
139+
for n in {
140+
(nnpdf_theory_card["mc"], 3),
141+
(nnpdf_theory_card["mc"], 4),
142+
(nnpdf_theory_card["mb"], 4),
143+
(nnpdf_theory_card["mb"], 5),
144+
}:
145+
assert any(np.allclose(n, x) for x in op_card_dict["mugrid"])
146+
147+
# Testing if the endpoints are correct
148+
np.testing.assert_allclose(op_card_dict["mugrid"][0], (q_ini, 3))
138149
np.testing.assert_allclose(op_card_dict["mugrid"][-1], (q_fin, 5))
150+
139151
# Testing computation of eko
140152
save_path = tmp_path / "ekotest.tar"
141153
runner.solve(t_card, op_card, save_path)

0 commit comments

Comments
 (0)