diff --git a/benchmarks/bench_evolve.py b/benchmarks/bench_evolve.py index c95122e9..27dd876a 100644 --- a/benchmarks/bench_evolve.py +++ b/benchmarks/bench_evolve.py @@ -6,13 +6,47 @@ import pineappl import pytest import yaml -from eko import couplings as sc import pineko import pineko.evolve import pineko.theory_card +@pytest.mark.parametrize("theoryid,is_mixed", [(400, False), (208, True)]) +def benchmark_write_operator_card_from_file_num_fonll( + tmp_path, test_files, test_configs, theoryid, is_mixed +): + tcard = pineko.theory_card.load(theoryid) + tcards_path_list = pineko.fonll.dump_tcards(tcard, tmp_path, theoryid) + pine_path = ( + test_files + / "data" + / "grids" + / "400" + / "HERA_NC_225GEV_EP_SIGMARED.pineappl.lz4" + ) + default_path = test_files / "data" / "operator_cards" / "400" / "_template.yaml" + num_opcard = 7 if is_mixed else 5 + targets_path_list = [ + tmp_path / f"test_opcard_{num}.yaml" for num in range(num_opcard) + ] + for target_path, tcard_path in zip(targets_path_list, tcards_path_list): + with open(tcard_path, encoding="utf-8") as f: + tcard = yaml.safe_load(f) + _x_grid, _q2_grid = pineko.evolve.write_operator_card_from_file( + pine_path, default_path, target_path, tcard + ) + # Check if the opcards are ok + for opcard_path, cfg in zip( + targets_path_list, + pineko.fonll.MIXED_FNS_CONFIG if is_mixed else pineko.fonll.FNS_CONFIG, + ): + with open(opcard_path, encoding="utf-8") as f: + ocard = yaml.safe_load(f) + for entry in ocard["mugrid"]: + assert entry[1] == int(cfg.nf) + + def benchmark_write_operator_card_from_file(tmp_path, test_files, test_configs): pine_path = test_files / "data/grids/400/HERA_NC_225GEV_EP_SIGMARED.pineappl.lz4" default_path = test_files / "data/operator_cards/400/_template.yaml" diff --git a/benchmarks/bench_fonll.py b/benchmarks/bench_fonll.py new file mode 100644 index 00000000..2f2381ff --- /dev/null +++ b/benchmarks/bench_fonll.py @@ -0,0 +1,28 @@ +import numpy as np +import pytest +import yaml + +import pineko + + +@pytest.mark.parametrize("theoryid,is_mixed", [(400, False), (208, True)]) +def benchmark_produce_fonll_tcards( + tmp_path, test_files, test_configs, theoryid, is_mixed +): + tcard = pineko.theory_card.load(theoryid) + tcard_paths_list = pineko.fonll.dump_tcards(tcard, tmp_path, theoryid) + # Check they are correct + theorycards = [] + for path in tcard_paths_list: + with open(path, encoding="UTF-8") as f: + theorycards.append(yaml.safe_load(f)) + base_pto = pineko.fonll.FNS_BASE_PTO[tcard["FNS"]] + for num_fonll_tcard, cfg in zip( + theorycards, + pineko.fonll.MIXED_FNS_CONFIG if is_mixed else pineko.fonll.FNS_CONFIG, + ): + po = int(base_pto) + (cfg.delta_pto if is_mixed else 0) + assert num_fonll_tcard["FNS"] == cfg.scheme + assert num_fonll_tcard["NfFF"] == int(cfg.nf) + assert num_fonll_tcard["PTO"] == po - 1 if is_mixed and cfg.asy else po + assert num_fonll_tcard["FONLLParts"] == cfg.parts diff --git a/src/pineko/check.py b/src/pineko/check.py index 5f893a3f..8f03a3ae 100644 --- a/src/pineko/check.py +++ b/src/pineko/check.py @@ -116,29 +116,49 @@ def check_grid_and_eko_compatible(pineappl_grid, operators, xif, max_as, max_al) raise ValueError("x grid in pineappl grid and eko operator are NOT compatible!") -def is_fonll_b(fns, lumi): - """Check if the fktable we are computing is a DIS FONLL-B fktable. +def is_dis(lumi): + """Check if the fktable we are computing is a DIS fktable. Parameters ---------- - fns : str - flavor number scheme (from the theory card) lumi : list(list(tuple)) luminosity info Returns ------- bool - true if the fktable is a FONLL-B DIS fktable + true if the fktable is a DIS fktable """ - for lists in lumi: - for el in lists: + for entry in lumi: + for el in entry: if (not islepton(el[0])) and (not islepton(el[1])): - # in this case we are sure it is not DIS so for sure it is not FONLL-B + # If neither of the incoming particles is a lepton we are sure + # it is not DIS return False - if fns == "FONLL-B": - return True - return False + return True + + +def is_fonll_mixed(fns, lumi): + """Check if the fktable we are computing is FONLL-B, FONLL-D or, in general, a mixed FONLL fktable. + + Parameters + ---------- + fns : str + flavor number scheme (from the theory card) + lumi : list(list(tuple)) + luminosity info + + Returns + ------- + bool + true if the fktable is a mixed FONLL DIS fktable + """ + return is_dis(lumi) and fns in ["FONLL-B", "FONLL-D"] + + +def is_num_fonll(fns): + """Check if the FNS is a nFONLL FNS.""" + return fns in ["FONLL-FFNS", "FONLL-FFN0"] def orders(grid, max_as, max_al) -> list: diff --git a/src/pineko/cli/__init__.py b/src/pineko/cli/__init__.py index bdd7ebe8..6b04f67a 100644 --- a/src/pineko/cli/__init__.py +++ b/src/pineko/cli/__init__.py @@ -1,3 +1,13 @@ """CLI entry point.""" -from . import check, compare, convolute, gen_sv, kfactor, opcard, scaffold, theory_ +from . import ( + check, + compare, + convolute, + fonll, + gen_sv, + kfactor, + opcard, + scaffold, + theory_, +) from ._base import command diff --git a/src/pineko/cli/fonll.py b/src/pineko/cli/fonll.py new file mode 100644 index 00000000..30a29c98 --- /dev/null +++ b/src/pineko/cli/fonll.py @@ -0,0 +1,174 @@ +"""CLI entry point to FONLL.""" +import pathlib + +import click +import rich + +from .. import configs, fonll, parser, theory_card +from ._base import command + +config_setting = click.option( + "-c", + "--configs", + "cfg", + default=None, + type=click.Path(resolve_path=True, path_type=pathlib.Path), + help="Explicitly specify config file (it has to be a valid TOML file).", +) + + +class TheoryCardError(Exception): + """Raised when asked for FONLL theory cards with an original tcard as input that is not asking for FONLL.""" + + +class InconsistentInputsError(Exception): + """Raised if the inputs are not consistent with FONLL.""" + + +def cfgpath(name, grid): + """Path of the fktable in 'name' called 'grid' if it exists, else None.""" + path = configs.configs["paths"]["fktables"] / name / grid + return path if path.exists() else None + + +def grids_names(yaml_file): + """Return the list of the grids in the yaml file.""" + yaml_content = parser._load_yaml(yaml_file) + # Turn the operands and the members into paths (and check all of them exist) + ret = [] + for operand in yaml_content["operands"]: + for member in operand: + ret.append(f"{member}.{parser.EXT}") + return ret + + +@command.command("combine_fonll") +@click.argument("theoryID", type=int) +@click.argument("dataset", type=str) +@click.option("--FFNS3", type=int, help="theoryID containing the ffns3 fktable") +@click.option("--FFN03", type=int, help="theoryID containing the ffn03 fktable") +@click.option("--FFNS4", type=int, help="theoryID containing the ffns4 fktable") +@click.option("--FFNS4til", type=int, help="theoryID containing the ffns4til fktable") +@click.option("--FFNS4bar", type=int, help="theoryID containing the ffns4bar fktable") +@click.option("--FFN04", type=int, help="theoryID containing the ffn04 fktable") +@click.option("--FFNS5", type=int, help="theoryID containing the ffns5 fktable") +@click.option("--FFNS5til", type=int, help="theoryID containing the ffns5til fktable") +@click.option("--FFNS5bar", type=int, help="theoryID containing the ffns5bar fktable") +@click.option("--overwrite", is_flag=True, help="Allow files to be overwritten") +@config_setting +def subcommand( + theoryid, + dataset, + ffns3, + ffn03, + ffns4, + ffns4til, + ffns4bar, + ffn04, + ffns5, + ffns5til, + ffns5bar, + overwrite, + cfg, +): + """Combine the different FKs needed to produce the FONLL prescription.""" + path = configs.detect(cfg) + base_configs = configs.load(path) + configs.configs = configs.defaults(base_configs) + if cfg is not None: + print(f"Configurations loaded from '{path}'") + + # Checks + + if not ffns3 or not ffn03: + raise InconsistentInputsError("ffns3 and/or ffn03 is not provided.") + + if any([ffns4, ffns4til, ffns4bar]): + if ffns4: + if any([ffns4til, ffns4bar]): + raise InconsistentInputsError( + "If ffns4 is provided no ffnstil or ffnsbar should be provided." + ) + else: + if ffns4til is None or ffns4bar is None: + raise InconsistentInputsError( + "if ffnstil is provided also ffnsbar should be provided, and vice versa." + ) + else: + raise InconsistentInputsError("ffns4 is not provided.") + + # Do we consider two masses, i.e. mc and mb + two_masses = False + if any([ffns5, ffns5til, ffns5bar]): + two_masses = True + if ffns5: + if any([ffns5til, ffns5bar]): + raise InconsistentInputsError( + "If ffns5 is provided no ffnstil or ffnsbar should be provided." + ) + else: + if ffns5til is None or ffns5bar is None: + raise InconsistentInputsError( + "if ffnstil is provided also ffnsbar should be provided, and vice versa." + ) + + if (ffn04 is None and two_masses) or (ffn04 is not None and not two_masses): + raise InconsistentInputsError( + "If two masses are to be considered, both ffn04 and the nf=5 coefficient should be provided" + ) + + # Get theory info + tcard = theory_card.load(theoryid) + if not "DAMPPOWER" in tcard: + if tcard["DAMP"] != 0: + raise InconsistentInputsError("If DAMP is set, set also DAMPPOWER") + tcard["DAMPPOWER"] = None + # Getting the paths to the grids + grids_name = grids_names(configs.configs["paths"]["ymldb"] / f"{dataset}.yaml") + for grid in grids_name: + # Checking if it already exists + new_fk_path = configs.configs["paths"]["fktables"] / str(theoryid) / grid + if new_fk_path.exists(): + if not overwrite: + rich.print( + f"[green]Success:[/] skipping existing FK Table {new_fk_path}" + ) + return + fonll.produce_combined_fk( + *( + cfgpath(str(name), grid) + for name in ( + ffns3, + ffn03, + ffns4, + ffns4til, + ffns4bar, + ffn04, + ffns5, + ffns5til, + ffns5bar, + ) + ), + theoryid, + damp=(tcard["DAMP"], tcard["DAMPPOWER"]), + cfg=cfg, + ) + if new_fk_path.exists(): + rich.print(f"[green]Success:[/] Wrote FK table to {new_fk_path}") + else: + rich.print(f"[red]Failure:[/]") + + +@command.command("fonll_tcards") +@click.argument("theoryID", type=int) +@config_setting +def fonll_tcards(theoryid, cfg): + """Produce the FONLL tcards starting from the original tcard given by the theoryID.""" + path = configs.detect(cfg) + base_configs = configs.load(path) + configs.configs = configs.defaults(base_configs) + tcard = theory_card.load(theoryid) + tcard_parent_path = theory_card.path(theoryid).parent + if "FONLL" not in tcard["FNS"]: + raise TheoryCardError("The theorycard does not correspond to an FONLL scheme.") + fonll.dump_tcards(tcard, tcard_parent_path, theoryid) diff --git a/src/pineko/evolve.py b/src/pineko/evolve.py index 7086cc5e..70796f50 100644 --- a/src/pineko/evolve.py +++ b/src/pineko/evolve.py @@ -105,7 +105,8 @@ def write_operator_card(pineappl_grid, default_card, card_path, tcard): """ # Add a +1 to the orders for the difference in convention between nnpdf and pineappl - is_fns = int(check.is_fonll_b(tcard["FNS"], pineappl_grid.lumi())) + # NB: This would not happen for nFONLL + is_fns = int(check.is_fonll_mixed(tcard["FNS"], pineappl_grid.lumi())) max_as = 1 + tcard["PTO"] + is_fns max_al = 1 + tcard["QED"] # ... in order to create a mask ... @@ -128,9 +129,14 @@ def write_operator_card(pineappl_grid, default_card, card_path, tcard): matching_scales=heavy_quarks.MatchingScales(masses * thresholds_ratios), origin=(tcard["Q0"] ** 2, tcard["nf0"]), ) - operators_card["mugrid"] = [ - (float(np.sqrt(q2)), int(nf_default(q2, atlas))) for q2 in q2_grid - ] + # If we are producing nFONLL FKs we need to look to NfFF... + if check.is_num_fonll(tcard["FNS"]): + nf = tcard["NfFF"] + operators_card["mugrid"] = [(float(np.sqrt(q2)), int(nf)) for q2 in q2_grid] + else: + operators_card["mugrid"] = [ + (float(np.sqrt(q2)), nf_default(q2, atlas)) for q2 in q2_grid + ] if "integrability_version" in pineappl_grid.key_values(): x_grid = evol_info.x1 x_grid = np.append(x_grid, 1.0) diff --git a/src/pineko/fonll.py b/src/pineko/fonll.py new file mode 100644 index 00000000..3075e227 --- /dev/null +++ b/src/pineko/fonll.py @@ -0,0 +1,275 @@ +"""Module to manage FONLL predictions.""" + +import copy +import dataclasses +import json +import logging +import tempfile +from pathlib import Path + +import pineappl +import rich +import yaml + +from . import configs + +logger = logging.getLogger(__name__) + +FNS_BASE_PTO = {"FONLL-A": 1, "FONLL-B": 1, "FONLL-C": 2, "FONLL-D": 2, "FONLL-E": 3} +"""Mapping between pertubative orders as in the theory card PTO and the FONLL scheme names. + +The explict mapping is the following (evolution, massive parts, massless parts): + * A: (1,1,1) + * B: (1,2,1) + * C: (2,2,2) + * D: (2,3,2) + * E: (3,3,3) +""" +MIXED_ORDER_FNS = ["FONLL-B", "FONLL-D"] +"""FONLL schemes with mixed orders.""" +# Notice we rely on the order defined by the FONLLInfo class +FK_TO_DAMP = { + "mc": ["ffn03", "ffns4til", "ffn04", "ffns5til"], + "mb": ["ffn04", "ffns5til"], +} +FK_WITH_MINUS = ["ffn03", "ffn04"] # asy terms should be subtracted, therefore the sign +"""FNS schemes to be subtracted during the FONLL procedure.""" + + +class FONLLInfo: + """Class containing all the information for FONLL predictions.""" + + def __init__( + self, ffns3, ffn03, ffns4, ffns4til, ffns4bar, ffn04, ffns5, ffns5til, ffns5bar + ) -> None: + """Initialize fonll info.""" + self.paths = { + "ffns3": ffns3, + "ffn03": ffn03, + "ffns4": ffns4, + "ffns4til": ffns4til, + "ffns4bar": ffns4bar, + "ffn04": ffn04, + "ffns5": ffns5, + "ffns5til": ffns5til, + "ffns5bar": ffns5bar, + } + actually_existing_paths = [p for p in self.paths if self.paths[p] is not None] + for p in self.paths: + if p not in actually_existing_paths: + logger.warning( + f"Warning! FK table for {p} does not exist and thus is being skipped." + ) + + @property + def fk_paths(self): + """Returns the list of the FK table paths needed to produce FONLL predictions.""" + return {p: Path(self.paths[p]) for p in self.paths if self.paths[p] is not None} + + @property + def fks(self): + """Returns the pineappl.Grid objects reading the paths provided by self.fk_paths.""" + # recall that FK tables are just a special grid + return {fk: pineappl.grid.Grid.read(path) for fk, path in self.fk_paths.items()} + + @property + def dataset_name(self): + """Return the name of the underlaying dataset.""" + names = {self.fk_paths[p].name for p in self.fk_paths} + if len(names) == 1: + return names.pop() + raise ValueError("Not all FK tables share the same name") + + @property + def theorycard_no_fns_pto(self): + """Return the common theory info between the different FONLL FK tables.""" + theorycards = [json.loads(self.fks[p].key_values()["theory"]) for p in self.fks] + # Only these should differ + for card in theorycards: + del card["FNS"] + del card["PTO"] + card.pop("PTODIS", None) + del card["NfFF"] + del card["ID"] + del card["FONLLParts"] + del card["Comments"] + if len(theorycards) > 1 and not all( + [theorycards[0] == card for card in theorycards[1:]] + ): + raise ValueError("Theory cards are not compatible") + return theorycards[0] + + @property + def Q2grid(self): + """The Q2grid of the (DIS) FK tables.""" + return self.fks[list(self.fks)[0]].bin_left(0) + + +def update_fk_theorycard(combined_fk, input_theorycard_path): + """Update theorycard entries for the combined fktable by reading the yamldb of the original theory.""" + with open(input_theorycard_path) as f: + final_theorycard = yaml.safe_load(f) + theorycard = json.loads(combined_fk.key_values()["theory"]) + theorycard["FNS"] = final_theorycard["FNS"] + theorycard["PTO"] = final_theorycard["PTO"] + theorycard["NfFF"] = final_theorycard["NfFF"] + theorycard["ID"] = final_theorycard["ID"] + # Update the theorycard with the entries set above + combined_fk.set_key_value("theory", str(theorycard)) + + +def produce_dampings(theorycard_constituent_fks, fonll_info, damp): + """Return the damping factors for each of the relevant masses.""" + mc = theorycard_constituent_fks["mc"] + mb = theorycard_constituent_fks["mb"] + q2grid = fonll_info.Q2grid + step_function_charm = mc**2 < q2grid + step_function_bottom = mb**2 < q2grid + damping_factor_charm = (1 - mc / q2grid) ** damp[1] + damping_factor_bottom = (1 - mb / q2grid) ** damp[1] + damping_factor_charm *= step_function_charm + damping_factor_bottom *= step_function_bottom + return {"mc": damping_factor_charm, "mb": damping_factor_bottom} + + +def combine(fk_dict, dampings=None): + """Rescale, eventually using dampings, and combine the sub FK tables.""" + # pineappl does not support operating with two grids in memory: + # https://github.com/NNPDF/pineappl/blob/8a672bef6d91b07a4edfdefbe4e30e4b1dd1f976/pineappl_py/src/grid.rs#L614-L617 + with tempfile.TemporaryDirectory() as tmpdirname: + combined_fk = fk_dict[list(fk_dict)[0]] + for fk in list(fk_dict)[1:]: + tmpfile_path = Path(tmpdirname) / f"{fk}.pineappl.lz4" + sign = -1 if fk in FK_WITH_MINUS else 1 + fk_dict[fk].scale(sign) + if dampings is not None: + for mass in FK_TO_DAMP: + if fk in FK_TO_DAMP[mass]: + fk_dict[fk].scale_by_bin(dampings[mass]) + fk_dict[fk].write_lz4(tmpfile_path) + combined_fk.merge_from_file(tmpfile_path) + return combined_fk + + +def produce_combined_fk( + ffns3, + ffn03, + ffns4, + ffns4til, + ffns4bar, + ffn04, + ffns5, + ffns5til, + ffns5bar, + theoryid, + damp=(0, None), + cfg=None, +): + """Combine the FONLL FK tables into one single FK table.""" + fonll_info = FONLLInfo( + ffns3, ffn03, ffns4, ffns4til, ffns4bar, ffn04, ffns5, ffns5til, ffns5bar + ) + theorycard_constituent_fks = fonll_info.theorycard_no_fns_pto + fk_dict = fonll_info.fks + dampings = ( + None + if damp[0] == 0 + else produce_dampings(theorycard_constituent_fks, fonll_info, damp) + ) + combined_fk = combine(fk_dict, dampings=dampings) + input_theorycard_path = ( + Path(configs.load(configs.detect(cfg))["paths"]["theory_cards"]) + / f"{theoryid}.yaml" + ) + update_fk_theorycard(combined_fk, input_theorycard_path) + # save final FONLL fktable + fk_folder = Path(configs.load(configs.detect(cfg))["paths"]["fktables"]) / str( + theoryid + ) + fk_folder.mkdir(exist_ok=True) + output_path_fk = fk_folder / fonll_info.dataset_name + combined_fk.write_lz4(output_path_fk) + + +@dataclasses.dataclass +class SubTheoryConfig: + """Single (sub-)theory configuration.""" + + asy: bool + nf: int + parts: str + delta_pto: int = 0 + + @property + def scheme(self): + """Yadism scheme name.""" + return "FONLL-FFN" + ("0" if self.asy else "S") + + +MIXED_FNS_CONFIG = [ + SubTheoryConfig(False, 3, "full", 1), + SubTheoryConfig(True, 3, "full", 1), + SubTheoryConfig(False, 4, "massless"), + SubTheoryConfig(False, 4, "massive", 1), + SubTheoryConfig(True, 4, "full", 1), + SubTheoryConfig(False, 5, "massless"), + SubTheoryConfig(False, 5, "massive", 1), +] +"""Mixed FONLL schemes.""" + +FNS_CONFIG = [ + SubTheoryConfig(False, 3, "full"), + SubTheoryConfig(True, 3, "full"), + SubTheoryConfig(False, 4, "full"), + SubTheoryConfig(True, 4, "full"), + SubTheoryConfig(False, 5, "full"), +] +"""Plain FONLL schemes.""" + + +def collect_updates(fonll_fns, damp): + """Produce the different theory cards according to which FONLL is asked for.""" + updates = [] + is_mixed = fonll_fns in MIXED_ORDER_FNS + is_damped = damp != 0 + base_pto = FNS_BASE_PTO[fonll_fns] + cfgs = MIXED_FNS_CONFIG if is_mixed or is_damped else FNS_CONFIG + for cfg in cfgs: + po = int(base_pto) + (cfg.delta_pto if is_mixed else 0) + updates.append( + { + "FNS": cfg.scheme, + "NfFF": cfg.nf, + "PTO": po, + "FONLLParts": cfg.parts, + } + ) + # In a mixed FONLL scheme we only subract the resummed terms that are + # present in the FFNS scheme at nf+1. E.g. for FONLL-B in FFN03 we + # only subract up to NLL since there is no NNLL in FFNS4 + if fonll_fns in MIXED_ORDER_FNS and cfg.asy: + updates[-1]["PTODIS"] = po + updates[-1]["PTO"] = po - 1 + return updates + + +def dump_tcards(tcard, tcard_parent_path, theoryid): + """Produce the seven FONLL theory cards from the original one. + + The produced theory cards are dumped in `tcard_parent_path` with names from '{theoryid}00.yaml' to '{theoryid}06.yaml'. + """ + updates = collect_updates(tcard["FNS"], tcard["DAMP"]) + n_theory = len(updates) + theorycards = [copy.deepcopy(tcard) for _ in range(n_theory)] + paths_list = [] + for num, (theorycard, recipe) in enumerate(zip(theorycards, updates)): + # update cards entries + theorycard.update(recipe) + theorycard["ID"] = int(f"{theoryid}0{num}") + # save + theorycard_path = tcard_parent_path / f'{theorycard["ID"]}.yaml' + with open(theorycard_path, "w", encoding="UTF-8") as f: + yaml.safe_dump(theorycard, f) + paths_list.append(theorycard_path) + rich.print(f"[green]Wrote theory card to {theorycard_path}") + return paths_list diff --git a/src/pineko/parser.py b/src/pineko/parser.py index a24737eb..43cb8e84 100644 --- a/src/pineko/parser.py +++ b/src/pineko/parser.py @@ -57,7 +57,6 @@ def get_yaml_information(yaml_file, grids_folder): List (of lists) with all the grids that will need to be loaded """ yaml_content = _load_yaml(yaml_file) - # Turn the operands and the members into paths (and check all of them exist) ret = [] for operand in yaml_content["operands"]: diff --git a/src/pineko/theory.py b/src/pineko/theory.py index dbacb5ae..d7ed3cfb 100644 --- a/src/pineko/theory.py +++ b/src/pineko/theory.py @@ -364,6 +364,14 @@ def fk(self, name, grid_path, tcard, pdf): do_log = self.activate_logging( paths["logs"]["fk"], f"{self.theory_id}-{name}-{pdf}.log" ) + + # Relevant for FONLL-B and FONLL-D: For FFN0 terms, PTO is lower than + # PTODIS, thus using PTO instead of PTODIS to establish the perturbative + # order would result in the PTODIS terms that correspond to orders + # beyond PTO to be neglected + if "PTODIS" in tcard and "FONLL" in tcard["FNS"]: + tcard["PTO"] = tcard["PTODIS"] + # check if grid contains SV if theory is requesting them (in particular # if theory is requesting scheme A or C) sv_method = evolve.sv_scheme(tcard) @@ -382,12 +390,26 @@ def fk(self, name, grid_path, tcard, pdf): return max_as = 1 + int(tcard["PTO"]) # Check if we are computing FONLL-B fktable and eventually change max_as - if check.is_fonll_b( + if check.is_fonll_mixed( tcard["FNS"], grid.lumi(), ): max_as += 1 + + # NB: This would not happen for nFONLL max_al = 0 + + # check if the grid is empty + if check.is_num_fonll(tcard["FNS"]): + if ( + pineappl.grid.Order.create_mask( + grid.orders(), max_as, max_al, True + ).size + == 0 + ): + rich.print(f"[green] Skipping empty grid.") + return + # check for sv if not np.isclose(xir, 1.0): check_scvar_evolve(grid, max_as, max_al, check.Scale.REN) @@ -396,7 +418,13 @@ def fk(self, name, grid_path, tcard, pdf): check_scvar_evolve(grid, max_as, max_al, check.Scale.FACT) # loading ekos to produce a tmp copy with eko.EKO.read(eko_filename) as operators: - eko_tmp_path = operators.paths.root.parent / "eko-tmp.tar" + # Skip the computation of the fktable if the eko is empty + if len(operators.mu2grid) == 0 and check.is_num_fonll(tcard["FNS"]): + rich.print(f"[green] Skipping empty eko for nFONLL.") + return + eko_tmp_path = ( + operators.paths.root.parent / f"eko-tmp-{name}-{np.random.rand()}.tar" + ) operators.deepcopy(eko_tmp_path) with eko.EKO.edit(eko_tmp_path) as operators: # Obtain the assumptions hash diff --git a/tests/test_check.py b/tests/test_check.py index fc6181a9..e616e825 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -20,18 +20,36 @@ def test_in1d(): assert checked == np.array([True]) -def test_is_fonll_b(): +def test_is_dis(): + dis_fake_lumi = [[(1, -13, 1.0), (11, 2, 2.0)]] + nondis_fake_lumi = [[(1, 2, 1.5), (2, 1, 3.0)]] + assert pineko.check.is_dis(dis_fake_lumi) is True + assert pineko.check.is_dis(nondis_fake_lumi) is False + + +def test_is_fonll_mixed(): fns = "FONLL-B" lumi_first = [[(-12, 1, 2.0), (-13, 1, 5.0)]] lumi_second = [[(1, 11, 1.0), (3, 11, 5.0)]] - assert pineko.check.is_fonll_b(fns, lumi_first) is True - assert pineko.check.is_fonll_b(fns, lumi_second) is True + assert pineko.check.is_fonll_mixed(fns, lumi_first) is True + assert pineko.check.is_fonll_mixed(fns, lumi_second) is True lumi_crazy = [[(1, 1, 4.0), (2, 11, 3.0)]] - assert pineko.check.is_fonll_b(fns, lumi_crazy) is False + assert pineko.check.is_fonll_mixed(fns, lumi_crazy) is False fns = "FONLL-C" - assert pineko.check.is_fonll_b(fns, lumi_first) is False - assert pineko.check.is_fonll_b(fns, lumi_second) is False - assert pineko.check.is_fonll_b(fns, lumi_crazy) is False + assert pineko.check.is_fonll_mixed(fns, lumi_first) is False + assert pineko.check.is_fonll_mixed(fns, lumi_second) is False + assert pineko.check.is_fonll_mixed(fns, lumi_crazy) is False + fns = "FONLL-D" + assert pineko.check.is_fonll_mixed(fns, lumi_first) is True + assert pineko.check.is_fonll_mixed(fns, lumi_second) is True + assert pineko.check.is_fonll_mixed(fns, lumi_crazy) is False + + +def test_is_num_fonll(): + num_fonll_FNS = "FONLL-FFN0" + non_num_fonll_FNS = "FONLL-B" + assert pineko.check.is_num_fonll(num_fonll_FNS) is True + assert pineko.check.is_num_fonll(non_num_fonll_FNS) is False class Fake_grid: diff --git a/tests/test_fonll.py b/tests/test_fonll.py new file mode 100644 index 00000000..bb947ad1 --- /dev/null +++ b/tests/test_fonll.py @@ -0,0 +1,60 @@ +import pathlib + +import pineko + + +def test_FONLLInfo(): + full_list = [ + "ffns3.pineappl.lz4", + "ffn03.pineappl.lz4", + "ffns4.pineappl.lz4", + "ffns4til.pineappl.lz4", + "ffns4bar.pineappl.lz4", + "ffn04.pineappl.lz4", + "ffns5.pineappl.lz4", + "ffns5til.pineappl.lz4", + "ffns5bar.pineappl.lz4", + ] + fullfonll_fake_info = pineko.fonll.FONLLInfo(*full_list) + wrongfonll_fake_info = pineko.fonll.FONLLInfo( + full_list[0], + full_list[1], + None, + full_list[3], + full_list[4], + None, + None, + None, + full_list[8], + ) + partialfonll_fake_info = pineko.fonll.FONLLInfo( + full_list[0], full_list[1], None, full_list[3], None, None, None, None, None + ) + name_list = [ + "ffns3", + "ffn03", + "ffns4", + "ffns4til", + "ffns4bar", + "ffn04", + "ffns5", + "ffns5til", + "ffns5bar", + ] + assert fullfonll_fake_info.fk_paths == { + name: pathlib.Path(fk) for name, fk in zip(name_list, full_list) + } + assert wrongfonll_fake_info.fk_paths == { + name: pathlib.Path(fk) + for name, fk in zip( + name_list[:2] + name_list[3:5] + [name_list[-1]], + full_list[:2] + full_list[3:5] + [full_list[-1]], + ) + } + assert partialfonll_fake_info.fk_paths == { + name: pathlib.Path(fk) + for name, fk in zip( + name_list[:2] + name_list[3:4], full_list[:2] + full_list[3:4] + ) + if fk is not None + }