Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable nf=3 with evolven3fit_new. #1754

Merged
merged 6 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conda-recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ requirements:
- sphinxcontrib-bibtex
- curio >=1.0
- pineappl >=0.5.8
- eko >=0.13.4,<0.14
- eko >=0.13.5,<0.14
- banana-hep >=0.6.8
- fiatlux

Expand Down
92 changes: 49 additions & 43 deletions n3fit/src/evolven3fit_new/eko_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
}

NFREF_DEFAULT = 5
NF0_DEFAULT = 4


def construct_eko_cards(
Expand Down Expand Up @@ -58,51 +57,77 @@ def construct_eko_cards(
theory = Loader().check_theoryID(theoryID).get_description()
theory.pop("FNS")
theory.update(theory_card_dict)
if "nfref" not in theory:
theory["nfref"] = NFREF_DEFAULT
if "nf0" not in theory:
theory["nf0"] = NF0_DEFAULT


# Prepare the thresholds according to MaxNfPdf
thresholds = {"c": theory["kcThr"], "b": theory["kbThr"], "t": theory["ktThr"]}
if theory["MaxNfPdf"] < 5:
thresholds["b"] = np.inf
if theory["MaxNfPdf"] < 6:
thresholds["t"] = np.inf


if "nfref" not in theory:
theory["nfref"] = NFREF_DEFAULT

# Set nf_0 according to the fitting scale unless set explicitly
mu0 = theory["Q0"]
if "nf0" not in theory:
if mu0 < theory["mc"] * thresholds["c"]:
theory["nf0"] = 3
elif mu0 < theory["mb"] * thresholds["b"]:
theory["nf0"] = 4
elif mu0 < theory["mt"] * thresholds["t"]:
theory["nf0"] = 5
else:
theory["nf0"] = 6
scarlehoff marked this conversation as resolved.
Show resolved Hide resolved

# Setting the thresholds in the theory card to inf if necessary
theory.update({"kbThr":thresholds["b"], "ktThr":thresholds["t"] })
theory.update({"kbThr": thresholds["b"], "ktThr": thresholds["t"]})

# The Legacy function is able to construct a theory card for eko starting from an NNPDF theory
legacy_class = runcards.Legacy(theory, {})
theory_card = legacy_class.new_theory

# if Qedref = Qref alphaem is running, otherwise it's fixed
if theory["QED"] > 0:
if np.isclose(theory["Qref"], theory["Qedref"]):
theory_card.couplings.em_running = True

# construct operator card
# Generate the q2grid, if q_fin and q_points are None, use `nf0` to select a default
q2_grid = utils.generate_q2grid(
theory["Q0"],
mu0,
q_fin,
q_points,
{theory["mb"]: thresholds["b"], theory["mt"]: thresholds["t"]},
{
theory["mc"]: thresholds["c"],
theory["mb"]: thresholds["b"],
theory["mt"]: thresholds["t"],
},
theory["nf0"],
Copy link
Contributor

@giacomomagni giacomomagni Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand this work, so in principle is fine.
When you call generate_q2grid with both Q2s given nf0 is ignored. On contrary by default from the cli here we have Q2s to None so you actually want to use nf0, it's just a bit confusing, but I don't see a quick fix as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the nf0 is acting a bit like a secret parameter to generate the q2grid from the defaults.
I guess another option is not to call generate_q2grid when there is nothing to generate (just a default being used).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe that's more clean.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to point out: nf0 parameter is going to disappear, or better to move. Now that we are propagating evolution points from EKO, everything should be an evolution point (a point on a lane, rather than on the ladder), and also fitting scale will be an evolution point.

So nf0 should be always used, and never None (despite being always 4, at least for the time being - but remember that is 3 for perturbative charm). You should be allowed to discard the nf information only at the latest possible time (i.e. when generating the LHAPDF grid file).

nf0 is an EKO extension to the NNPDF theory database, and inferred from the comparison of Q0 with mc * kcThr, but it would be better to always have it explicit, and infer as little as possible.

)

# construct operator card
op_card = default_op_card
masses = np.array([theory["mc"], theory["mb"], theory["mt"]]) ** 2
thresholds_ratios = np.array([thresholds["c"], thresholds["b"], thresholds["t"]]) ** 2

atlas = Atlas(
matching_scales=MatchingScales(masses * thresholds_ratios),
origin=(theory["Q0"] ** 2, theory["nf0"]),
)
op_card.update(
{
"mu0": theory["Q0"],
"mugrid": [(float(np.sqrt(q2)), int(nf_default(q2, atlas))) for q2 in q2_grid],
}
origin=(mu0**2, theory["nf0"]),
)

# Create the eko operator q2grid
# This is a grid which contains information on (q, nf)
# in VFNS values at the matching scales need to be doubled so that they are considered in both sides

ep = 1e-4
mugrid = []
for q2 in q2_grid:
q = float(np.sqrt(q2))
if nf_default(q2 + ep, atlas) != nf_default(q2 - ep, atlas):
nf_l = int(nf_default(q2 - ep, atlas))
nf_u = int(nf_default(q2 + ep, atlas))
mugrid.append((q, nf_l))
mugrid.append((q, nf_u))
else:
mugrid.append((q, int(nf_default(q2, atlas))))

op_card.update({"mu0": theory["Q0"], "mugrid": mugrid})

op_card["xgrid"] = x_grid
# Specific defaults for evolven3fit evolution
if theory["ModEv"] == "TRN":
Expand All @@ -121,22 +146,3 @@ def construct_eko_cards(

op_card = runcards.OperatorCard.from_dict(op_card)
return theory_card, op_card


def split_evolgrid(evolgrid):
"""Split the evolgrid in blocks according to the number of flavors and repeating the last entry of one block in the first entry of the next."""
evolgrid_index_list = []
evolgrid.sort()
starting_nf = evolgrid[0][1]
for evo_point in evolgrid:
current_nf = evo_point[1]
if current_nf != starting_nf:
evolgrid_index_list.append(evolgrid.index(evo_point))
starting_nf = current_nf
start_index = 0
evolgrid_list = []
for index in evolgrid_index_list:
evolgrid_list.append(evolgrid[start_index : index + 1])
start_index = index
evolgrid_list.append(evolgrid[start_index:])
return evolgrid_list
23 changes: 16 additions & 7 deletions n3fit/src/evolven3fit_new/evolve.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import defaultdict
import logging
import pathlib
import sys
Expand Down Expand Up @@ -145,7 +146,7 @@ def load_fit(usr_path):
"""
nnfitpath = usr_path / "nnfit"
pdf_dict = {}
for yaml_file in nnfitpath.glob("replica_*/*.exportgrid"):
for yaml_file in nnfitpath.glob(f"replica_*/{usr_path.name}.exportgrid"):
scarlehoff marked this conversation as resolved.
Show resolved Hide resolved
data = yaml.safe_load(yaml_file.read_text(encoding="UTF-8"))
pdf_dict[yaml_file.parent.stem] = data
return pdf_dict
Expand Down Expand Up @@ -178,19 +179,27 @@ def evolve_exportgrid(exportgrid, eko, x_grid, qed):
# generate block to dump
targetgrid = eko.bases.targetgrid.tolist()

def ev_pdf(pid, x, Q2):
return x * evolved_pdf[Q2]["pdfs"][pid][targetgrid.index(x)]
# Finally separate by nf block (and order per nf/q)
by_nf = defaultdict(list)
for q, nf in sorted(eko.evolgrid, key=lambda ep: ep[1]):
by_nf[nf].append(q)
q2block_per_nf = {nf: sorted(qs) for nf, qs in by_nf.items()}

evolgrid_list = eko_utils.split_evolgrid(eko.evolgrid)
blocks = []
for evgrid in evolgrid_list:
for nf, q2grid in q2block_per_nf.items():

def pdf_xq2(pid, x, Q2):
x_idx = targetgrid.index(x)
return x * evolved_pdf[(Q2, nf)]["pdfs"][pid][x_idx]

block = genpdf.generate_block(
ev_pdf,
pdf_xq2,
xgrid=targetgrid,
evolgrid=evgrid,
sorted_q2grid=q2grid,
pids=basis_rotation.flavor_basis_pids,
)
blocks.append(block)

return blocks


Expand Down
74 changes: 69 additions & 5 deletions n3fit/src/evolven3fit_new/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from reportengine.compat import yaml
from validphys.pdfbases import PIDS_DICT

DEFAULT_Q2GRID = (
Q2GRID_Nf04 = (
np.array(
[
1.6500000e00,
Expand All @@ -22,7 +22,6 @@
3.8800751e00,
4.3584516e00,
4.9200000e00,
4.9200000e00,
5.5493622e00,
6.2897452e00,
7.1650687e00,
Expand Down Expand Up @@ -65,6 +64,62 @@
** 2
)

Q2GRID_Nf03 = (
np.array(
[
1.0000000e00,
1.0768843e00,
1.1642787e00,
1.2640247e00,
1.3783565e00,
1.5100000e00,
1.6573843e00,
1.8279487e00,
2.0263188e00,
2.2582323e00,
2.5308507e00,
2.8531703e00,
3.2365690e00,
3.6955380e00,
4.2486693e00,
4.9200000e00,
5.6571821e00,
6.5475141e00,
7.6300446e00,
8.9555329e00,
1.0590474e01,
1.2622686e01,
1.5169120e01,
1.8386905e01,
2.2489085e01,
2.7767274e01,
3.4624624e01,
4.3624282e01,
5.5561424e01,
7.1571582e01,
9.3295496e01,
1.2313315e02,
1.6464038e02,
2.2315640e02,
3.0681103e02,
4.2816505e02,
6.0692308e02,
8.7449251e02,
1.2817733e03,
1.9127020e03,
2.9082314e03,
4.5095982e03,
7.1379509e03,
1.1543948e04,
1.9094934e04,
3.2338760e04,
5.6137084e04,
1.0000000e05,
]
)
** 2
)


class LhapdfLike:
"""
Expand Down Expand Up @@ -123,15 +178,22 @@ def get_theoryID_from_runcard(usr_path):
return my_runcard["theory"]["theoryid"]


def generate_q2grid(Q0, Qfin, Q_points, match_dict):
def generate_q2grid(Q0, Qfin, Q_points, match_dict, nf0=None):
"""Generate the q2grid used in the final evolved pdfs or use the default grid if Qfin or Q_points is
not provided.

match_dict contains the couples (mass : factor) where factor is the number to be multiplied to mass
in order to obtain the relative matching scale.
"""
if Qfin is None and Q_points is None:
return DEFAULT_Q2GRID
if nf0 == 4:
scarlehoff marked this conversation as resolved.
Show resolved Hide resolved
return Q2GRID_Nf04
elif nf0 == 3:
return Q2GRID_Nf03
elif nf0 is None:
raise ValueError("In order to use a default grid, a value of nf0 must be provided")
else:
raise NotImplementedError(f"No default grid in Q available for {nf0=}")
elif Qfin is None or Q_points is None:
raise ValueError("q_fin and q_points must be specified either both or none of them")
else:
Expand All @@ -148,7 +210,9 @@ def generate_q2grid(Q0, Qfin, Q_points, match_dict):
frac_of_point = np.log(match_scale / Q_ini) / np.log(Qfin / Q0)
num_points = int(Q_points * frac_of_point)
num_points_list.append(num_points)
grids.append(np.geomspace(Q_ini**2, match_scale**2, num=num_points))
grids.append(
np.geomspace(Q_ini**2, match_scale**2, num=num_points, endpoint=False)
)
Q_ini = match_scale
num_points = Q_points - sum(num_points_list)
grids.append(np.geomspace(Q_ini**2, Qfin**2, num=num_points))
Expand Down
56 changes: 36 additions & 20 deletions n3fit/src/n3fit/tests/test_evolven3fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ def assert_sorted(arr, title):
raise ValueError(f"The values of {title} are not sorted!")


def check_consecutive_members(grid, value):
scarlehoff marked this conversation as resolved.
Show resolved Hide resolved
"""Check if the first occurrence of value in grid is followed by value again"""
return np.allclose(grid[list(grid).index(value) + 1], value)


def check_lhapdf_info(info_path):
"""Check the LHAPDF info file is correct"""
info = yaml.load(info_path.open("r", encoding="utf-8"))
Expand Down Expand Up @@ -77,24 +72,40 @@ def check_lhapdf_dat(dat_path, info):
# Use allclose here to avoid failing because of a different in the 7th place
np.testing.assert_allclose(q[-1], info["QMax"])


def test_generate_q2grid():
"""Tests the creation of the default grids for different values of nf
and whether the matched grid is generating points in the desired locations
"""
# nf 3, q0 = 1.0
grid = utils.generate_q2grid(None, None, None, {}, 3)
assert grid[0] == 1.0**2
# nf 4, q0 = 1.65
grid = utils.generate_q2grid(None, None, None, {}, 4)
assert grid[0] == 1.65**2

for nf in [1, 2, 5, 6]:
with pytest.raises(NotImplementedError):
grid = utils.generate_q2grid(None, None, None, {}, nf)

with pytest.raises(ValueError):
grid = utils.generate_q2grid(None, None, None, {})

def test_utils():
# Testing the default grid
grid = utils.generate_q2grid(1.65, None, None, {})
assert_allclose(1.65**2, grid[0])
assert len(grid) == 50
# We expect the bottom mass to be repeated twice because it is intended once in 4 flavors and once in 5 flavors.
assert check_consecutive_members(grid, 4.92**2)
# Testing if the points of the matching are correctly repeated twice
matched_grid = utils.generate_q2grid(1.65, 1.0e5, 100, {4.92: 2.0, 100: 1.0})
assert len(matched_grid) == 100
t1 = 4.92 * 2.0
t2 = 100.0 * 1.0

assert_allclose((1.65) ** 2, matched_grid[0])
assert_allclose((1.0e5) ** 2, matched_grid[-1])
assert check_consecutive_members(matched_grid, (4.92 * 2.0) ** 2)
assert check_consecutive_members(matched_grid, (100.0 * 1.0) ** 2)
assert t1**2 in matched_grid
assert t2**2 in matched_grid


def test_utils():
# Testing the fake LHAPDF class
q20 = 1.65**2
x_grid = np.geomspace(1.0e-7, 1.0, 30)
fake_grids = [[x * (1.0 - x) for x in x_grid] for pid in PIDS_DICT.keys()]
fake_grids = [[x * (1.0 - x) for x in x_grid] for _ in PIDS_DICT.keys()]
pdf_grid = dict([(pid, v) for pid, v in zip(range(len(PIDS_DICT)), fake_grids)])
my_PDF = utils.LhapdfLike(pdf_grid, q20, x_grid)
assert my_PDF.hasFlavor(6)
Expand Down Expand Up @@ -133,10 +144,15 @@ def test_eko_utils(tmp_path):
t_card_dict["order"][0] == pto + 1
) # This is due to a different convention in eko orders due to QED
assert_allclose(op_card_dict["xgrid"], x_grid)
assert_allclose(op_card_dict["mugrid"][0], (1.65, 4))
# In theory 162 the charm threshold is at 1.51
# and we should find two entries, one for nf=3 and another one for nf=4
assert_allclose(op_card_dict["mugrid"][0], (1.51, 3))
assert_allclose(op_card_dict["mugrid"][1], (1.51, 4))
# Then (with the number of points we chosen it will happen in position 2,3
# we will find the bottom threshold at two different nf
assert_allclose(op_card_dict["mugrid"][2], (4.92, 4))
assert_allclose(op_card_dict["mugrid"][3], (4.92, 5))
assert_allclose(op_card_dict["mugrid"][-1], (q_fin, 5))
# In this case there are not enough points to have twice the bottom matching scale
assert_allclose(op_card_dict["mugrid"][1], (4.92, 5))
# Testing computation of eko
save_path = tmp_path / "ekotest.tar"
runner.solve(t_card, op_card, save_path)
Expand Down