diff --git a/src/eko/__init__.py b/src/eko/__init__.py index 7636c7845..a8101420e 100644 --- a/src/eko/__init__.py +++ b/src/eko/__init__.py @@ -1,6 +1,7 @@ """Evolution Kernel Operators.""" from . import io, version +from .io.runcards import OperatorCard, TheoryCard from .io.struct import EKO from .runner import solve @@ -8,6 +9,8 @@ __all__ = [ "io", + "OperatorCard", + "TheoryCard", "EKO", "solve", ] diff --git a/src/eko/io/legacy.py b/src/eko/io/legacy.py deleted file mode 100644 index fdd7fbe9c..000000000 --- a/src/eko/io/legacy.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Support legacy storage formats.""" - -import dataclasses -import io -import pathlib -import tarfile -import tempfile -import warnings -from typing import Dict, List - -import lz4.frame -import numpy as np -import yaml - -from ..interpolation import XGrid -from ..io.runcards import flavored_mugrid -from ..quantities.heavy_quarks import ( - HeavyInfo, - HeavyQuarkMasses, - MatchingRatios, - QuarkMassScheme, -) -from . import raw -from .dictlike import DictLike -from .struct import EKO, Operator -from .types import EvolutionPoint as EPoint -from .types import RawCard, ReferenceRunning - -_MC = 1.51 -_MB = 4.92 -_MT = 172.5 - - -def load_tar(source: pathlib.Path, dest: pathlib.Path, errors: bool = False): - """Load tar representation from file. - - Compliant with :meth:`dump_tar` output. - - Parameters - ---------- - source : - source tar name - dest : - dest tar name - errors : - whether to load also errors (default ``False``) - """ - with tempfile.TemporaryDirectory() as tmpdirr: - tmpdir = pathlib.Path(tmpdirr) - - with tarfile.open(source, "r") as tar: - raw.safe_extractall(tar, tmpdir) - - # load metadata - innerdir = list(tmpdir.glob("*"))[0] - metapath = innerdir / "metadata.yaml" - metaold = yaml.safe_load(metapath.read_text(encoding="utf-8")) - - theory = PseudoTheory.from_old(metaold) - operator = PseudoOperator.from_old(metaold) - - # get actual grids - arrays = load_arrays(innerdir) - - op5 = metaold.get("Q2grid") - if op5 is None: - op5 = metaold["mu2grid"] - grid = op5to4( - flavored_mugrid(op5, [_MC, _MB, _MT], theory.heavy.matching_ratios), arrays - ) - - with EKO.create(dest) as builder: - # here I'm plainly ignoring the static analyzer, the types are faking - # the actual ones - not sure if I should fix builder interface to - # accept also these - eko = builder.load_cards(theory, operator).build() # pylint: disable=E1101 - - for ep, op in grid.items(): - eko[ep] = op - - eko.metadata.version = metaold.get("eko_version", "") - eko.metadata.data_version = 0 - eko.metadata.update() - - -@dataclasses.dataclass -class PseudoTheory(DictLike): - """Fake theory, mocking :class:`eko.io.runcards.TheoryCard`. - - Used to provide a theory for the :class:`~eko.io.struct.EKO` builder, even when the theory - information is not available. - """ - - heavy: HeavyInfo - - @classmethod - def from_old(cls, old: RawCard): - """Load from old metadata.""" - heavy = HeavyInfo( - masses=HeavyQuarkMasses( - [ - ReferenceRunning([_MC, np.inf]), - ReferenceRunning([_MB, np.inf]), - ReferenceRunning([_MT, np.inf]), - ] - ), - masses_scheme=QuarkMassScheme.POLE, - matching_ratios=MatchingRatios([1.0, 1.0, 1.0]), - ) - return cls(heavy=heavy) - - -@dataclasses.dataclass -class PseudoOperator(DictLike): - """Fake operator, mocking :class:`eko.io.runcards.OperatorCard`. - - Used to provide a theory for the :class:`~eko.io.struct.EKO` builder, even when the operator - information is not fully available. - """ - - init: EPoint - evolgrid: List[EPoint] - xgrid: XGrid - configs: dict - - @classmethod - def from_old(cls, old: RawCard): - """Load from old metadata.""" - mu0 = float(np.sqrt(float(old["q2_ref"]))) - mu2list = old.get("Q2grid") - if mu2list is None: - mu2list = old["mu2grid"] - mu2grid = np.array(mu2list) - evolgrid = flavored_mugrid( - np.sqrt(mu2grid).tolist(), [_MC, _MB, _MT], [1, 1, 1] - ) - - xgrid = XGrid(old["interpolation_xgrid"]) - - configs = dict( - interpolation_polynomial_degree=old.get("interpolation_polynomial_degree"), - interpolation_is_log=old.get("interpolation_is_log"), - ) - - return cls(init=(mu0, 4), evolgrid=evolgrid, xgrid=xgrid, configs=configs) - - -ARRAY_SUFFIX = ".npy.lz4" -"""Suffix for array files inside the tarball.""" - - -def load_arrays(dir: pathlib.Path) -> dict: - """Load arrays from compressed dumps.""" - arrays = {} - for fp in dir.glob(f"*{ARRAY_SUFFIX}"): - with lz4.frame.open(fp, "rb") as fd: - # static analyzer can not guarantee the content to be bytes - content = fd.read() - assert not isinstance(content, str), "Bytes expected" - - stream = io.BytesIO(content) - stream.seek(0) - arrays[pathlib.Path(fp.stem).stem] = np.load(stream) - - return arrays - - -OPERATOR = "operator" -"""File name stem for operators.""" -ERROR = "operator_error" -"""File name stem for operator errrors.""" - - -def op5to4(evolgrid: List[EPoint], arrays: dict) -> Dict[EPoint, Operator]: - """Load dictionary of 4-dim operators, from a single 5-dim one.""" - - def plural(name: str) -> list: - return [name, f"{name}s"] - - op5 = None # 5 dimensional operator - err5 = None - for name, ar in arrays.items(): - if name in plural(OPERATOR): - op5 = ar - elif name in plural(ERROR): - err5 = ar - else: - warnings.warn(f"Unrecognized array loaded, '{name}'") - - if op5 is None: - raise RuntimeError("Operator not found") - if err5 is None: - err5 = [None] * len(op5) - - grid = {} - for ep, op4, err4 in zip(evolgrid, op5, err5): - grid[ep] = Operator(operator=op4, error=err4) - - return grid diff --git a/src/eko/io/metadata.py b/src/eko/io/metadata.py index 779a3ece6..10058042b 100644 --- a/src/eko/io/metadata.py +++ b/src/eko/io/metadata.py @@ -7,6 +7,7 @@ from typing import Optional import yaml +from packaging.version import parse from .. import version as vmod from ..interpolation import XGrid @@ -59,9 +60,15 @@ def load(cls, path: os.PathLike): loaded metadata """ path = pathlib.Path(path) - content = cls.from_dict( - yaml.safe_load(InternalPaths(path).metadata.read_text(encoding="utf-8")) - ) + # read raw file first to catch version + raw = yaml.safe_load(InternalPaths(path).metadata.read_text(encoding="utf-8")) + version = parse(raw["version"]) + if version.major == 0 and version.minor == 13: + raise NotImplementedError("TODO") + elif version.major == 0 and version.minor == 14: + raise NotImplementedError("TODO") + else: + content = cls.from_dict(raw) content._path = path return content @@ -70,7 +77,7 @@ def update(self): if self._path is None: logger.info("Impossible to set metadata, no file attached.") else: - with open(InternalPaths(self._path).metadata, "w") as fd: + with open(InternalPaths(self._path).metadata, "w", encoding="utf8") as fd: yaml.safe_dump(self.raw, fd) @property diff --git a/src/eko/runner/__init__.py b/src/eko/runner/__init__.py index 14351b7f6..acb26359d 100644 --- a/src/eko/runner/__init__.py +++ b/src/eko/runner/__init__.py @@ -1,39 +1,6 @@ """Manage steps to DGLAP solution, and operator creation.""" -from pathlib import Path -from typing import Union - from ..io.runcards import OperatorCard, TheoryCard -from ..io.types import RawCard -from . import legacy - - -# TODO: drop this altogether, replacing just with managed.solve -# it is currently kept not to break the interface, but the runcards upgrade and -# path conversion should be done by the caller, here we just clearly declare -# which types we expect -def solve( - theory_card: Union[RawCard, TheoryCard], - operators_card: Union[RawCard, OperatorCard], - path: Path, -): - r"""Solve DGLAP equations in terms of evolution kernel operators (EKO). - - The EKO :math:`\mathbf E_{k,j}(a_s^1\leftarrow a_s^0)` is determined in order - to fullfill the following evolution - - .. math:: - \mathbf f(x_k,a_s^1) = \mathbf E_{k,j}(a_s^1\leftarrow a_s^0) \mathbf f(x_j,a_s^0) - - The configuration is split between the theory settings, representing - Standard Model parameters and other defining features of the theory - calculation, and the operator settings, those that are more closely related - to the solution of the |DGLAP| equation itself, and determine the resulting - operator features. +from .managed import solve - Note - ---- - For further information about EKO inputs and output see :doc:`/code/IO` - """ - # TODO: drop this - legacy.Runner(theory_card, operators_card, path).compute() +__all__ = ["OperatorCard", "TheoryCard", "solve"] diff --git a/src/eko/runner/legacy.py b/src/eko/runner/legacy.py deleted file mode 100644 index 56a9242f8..000000000 --- a/src/eko/runner/legacy.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Main application class of eko.""" - -import logging -from pathlib import Path -from typing import Union - -from ekomark.data import update_runcards - -from ..evolution_operator.grid import OperatorGrid -from ..io import EKO, Operator, runcards -from ..io.types import RawCard -from . import commons - -logger = logging.getLogger(__name__) - - -class Runner: - """Represents a single input configuration. - - For details about the configuration, see :doc:`here ` - - Attributes - ---------- - setup : dict - input configurations - """ - - banner = commons.BANNER - - def __init__( - self, - theory_card: Union[RawCard, runcards.TheoryCard], - operators_card: Union[RawCard, runcards.OperatorCard], - path: Path, - ): - """Initialize runner. - - Parameters - ---------- - theory_card : - theory parameters and options - operators_card : - operator specific options - path : - path where to store the computed operator - """ - new_theory, new_operator = update_runcards(theory_card, operators_card) - - # Store inputs - self.path = path - self._theory = new_theory - - # setup basis grid - bfd = commons.interpolator(new_operator) - - # call explicitly iter to explain the static analyzer that is an - # iterable - tc = commons.atlas(new_theory, new_operator) - - # strong coupling - cs = commons.couplings(new_theory, new_operator) # setup operator grid - - # compute masses if required - masses = runcards.masses(new_theory, new_operator.configs.evolution_method) - - self.op_grid = OperatorGrid( - mu2grid=new_operator.evolgrid, - order=new_theory.order, - masses=masses, - mass_scheme=new_theory.heavy.masses_scheme.value, - thresholds_ratios=new_theory.heavy.squared_ratios, - xif=new_theory.xif, - configs=new_operator.configs, - debug=new_operator.debug, - atlas=tc, - couplings=cs, - interpol_dispatcher=bfd, - n3lo_ad_variation=new_theory.n3lo_ad_variation, - use_fhmruvv=new_theory.use_fhmruvv, - matching_order=new_theory.matching_order, - ) - - with EKO.create(path) as builder: - builder.load_cards( # pylint: disable=E1101 - new_theory, new_operator - ).build() - - def compute(self): - """Run evolution and generate output operator. - - Two steps are applied sequentially: - - 1. evolution is performed, computing the evolution operator in an - internal flavor and x-space basis - 2. bases manipulations specified in the runcard are applied - """ - with EKO.edit(self.path) as eko: - # add all operators - for ep, op in self.op_grid.compute().items(): - eko[ep] = Operator(**op) diff --git a/src/ekobox/cli/convert.py b/src/ekobox/cli/convert.py index 260a96a91..6f5e2a567 100644 --- a/src/ekobox/cli/convert.py +++ b/src/ekobox/cli/convert.py @@ -1,26 +1,15 @@ """Upgrade old files.""" -import pathlib -from typing import Optional -import click - -from eko.io import legacy - -from .base import command - - -@command.command("convert") -@click.argument("old", type=click.Path(path_type=pathlib.Path, exists=True)) -@click.option("-n", "--new", type=click.Path(path_type=pathlib.Path), default=None) -def subcommand(old: pathlib.Path, new: Optional[pathlib.Path]): - """Convert old EKO files to new format. - - The OLD file path is used also for the new one, appending "-new" to - its stem, unless it is explicitly specified with the dedicated - option. - """ - if new is None: - new = old.parent / old.with_stem(old.stem + "-new") - - legacy.load_tar(old, new) +# @command.command("convert") +# @click.argument("old", type=click.Path(path_type=pathlib.Path, exists=True)) +# @click.option("-n", "--new", type=click.Path(path_type=pathlib.Path), default=None) +# def subcommand(old: pathlib.Path, new: Optional[pathlib.Path]): +# """Convert old EKO files to new format. + +# The OLD file path is used also for the new one, appending "-new" to +# its stem, unless it is explicitly specified with the dedicated +# option. +# """ +# if new is None: +# new = old.parent / old.with_stem(old.stem + "-new") diff --git a/src/ekobox/cli/inspect.py b/src/ekobox/cli/inspect.py index c355fce61..a322023b0 100644 --- a/src/ekobox/cli/inspect.py +++ b/src/ekobox/cli/inspect.py @@ -1,4 +1,4 @@ -"""Launch EKO calculations, with legacy mu2grid mode.""" +"""Inspect EKO content.""" import pathlib diff --git a/src/ekobox/cli/run.py b/src/ekobox/cli/run.py index e85a4c76f..4f97d26f7 100644 --- a/src/ekobox/cli/run.py +++ b/src/ekobox/cli/run.py @@ -1,4 +1,4 @@ -"""Launch EKO calculations, with legacy mu2grid mode.""" +"""Launch EKO calculations.""" import pathlib from typing import Sequence @@ -30,8 +30,9 @@ def subcommand(paths: Sequence[pathlib.Path]): 3. three arguments: same as two, but the third argument is used as output path - \b + Default names are: + - theory card: "theory.yaml" - operator card: "operator.yaml" - output: "eko.tar". diff --git a/tests/eko/evolution_operator/test_grid.py b/tests/eko/evolution_operator/test_grid.py index 66812c2fd..f06c152fc 100644 --- a/tests/eko/evolution_operator/test_grid.py +++ b/tests/eko/evolution_operator/test_grid.py @@ -5,81 +5,51 @@ test whether the result is correct, it can just test that it is sane """ -import enum -import pathlib -import numpy as np -import pytest - -import eko.io.types -from eko import couplings -from eko.quantities.couplings import CouplingEvolutionMethod -from eko.runner import legacy - - -def test_init_errors(monkeypatch, theory_ffns, operator_card, tmp_path, caplog): - # do some dance to fake the ev mode, but only that one - class FakeEM(enum.Enum): - BLUB = "blub" - - monkeypatch.setattr( - couplings, - "couplings_mod_ev", - lambda *args: CouplingEvolutionMethod.EXACT, - ) - operator_card.configs.evolution_method = FakeEM.BLUB - with pytest.raises(ValueError, match="BLUB"): - legacy.Runner(theory_ffns(3), operator_card, path=tmp_path / "eko.tar") - - # check LO - operator_card.configs.evolution_method = eko.io.types.EvolutionMethod.TRUNCATED - legacy.Runner(theory_ffns(3), operator_card, path=tmp_path / "eko.tar") - assert "LO" in caplog.text - assert "exact" in caplog.text - - -def test_compute_mu2grid(theory_ffns, operator_card, tmp_path): - mugrid = [(10.0, 5), (100.0, 5)] - operator_card.mugrid = mugrid - opgrid = legacy.Runner( - theory_ffns(3), operator_card, path=tmp_path / "eko.tar" - ).op_grid - opg = opgrid.compute() - assert len(opg) == len(mugrid) - assert all(k in op for k in ["operator", "error"] for op in opg.values()) - - -def test_grid_computation_VFNS(theory_card, operator_card, tmp_path): - """Checks that the grid can be computed.""" - mugrid = [(3, 4), (5, 5), (5, 4)] - operator_card.mugrid = mugrid - opgrid = legacy.Runner( - theory_card, operator_card, path=tmp_path / "eko.tar" - ).op_grid - operators = opgrid.compute() - assert len(operators) == len(mugrid) - - -def test_mod_expanded(theory_card, theory_ffns, operator_card, tmp_path: pathlib.Path): - mugrid = [(3, 4)] - operator_card.mugrid = mugrid - operator_card.configs.scvar_method = eko.io.types.ScaleVariationsMethod.EXPANDED - epsilon = 1e-1 - path = tmp_path / "eko.tar" - for is_ffns, nf0 in zip([False, True], [5, 3]): - if is_ffns: - theory = theory_ffns(nf0) - else: - theory = theory_card - theory.order = (1, 0) - operator_card.init = (operator_card.init[0], nf0) - path.unlink(missing_ok=True) - opgrid = legacy.Runner(theory, operator_card, path=path).op_grid - opg = opgrid.compute() - theory.xif = 1.0 + epsilon - path.unlink(missing_ok=True) - sv_opgrid = legacy.Runner(theory, operator_card, path=path).op_grid - sv_opg = sv_opgrid.compute() - np.testing.assert_allclose( - opg[(9, 4)]["operator"], sv_opg[(9, 4)]["operator"], atol=2.5 * epsilon - ) +# from eko.runner import legacy + +# def test_compute_mu2grid(theory_ffns, operator_card, tmp_path): +# mugrid = [(10.0, 5), (100.0, 5)] +# operator_card.mugrid = mugrid +# opgrid = legacy.Runner( +# theory_ffns(3), operator_card, path=tmp_path / "eko.tar" +# ).op_grid +# opg = opgrid.compute() +# assert len(opg) == len(mugrid) +# assert all(k in op for k in ["operator", "error"] for op in opg.values()) + + +# def test_grid_computation_VFNS(theory_card, operator_card, tmp_path): +# """Checks that the grid can be computed.""" +# mugrid = [(3, 4), (5, 5), (5, 4)] +# operator_card.mugrid = mugrid +# opgrid = legacy.Runner( +# theory_card, operator_card, path=tmp_path / "eko.tar" +# ).op_grid +# operators = opgrid.compute() +# assert len(operators) == len(mugrid) + + +# def test_mod_expanded(theory_card, theory_ffns, operator_card, tmp_path: pathlib.Path): +# mugrid = [(3, 4)] +# operator_card.mugrid = mugrid +# operator_card.configs.scvar_method = eko.io.types.ScaleVariationsMethod.EXPANDED +# epsilon = 1e-1 +# path = tmp_path / "eko.tar" +# for is_ffns, nf0 in zip([False, True], [5, 3]): +# if is_ffns: +# theory = theory_ffns(nf0) +# else: +# theory = theory_card +# theory.order = (1, 0) +# operator_card.init = (operator_card.init[0], nf0) +# path.unlink(missing_ok=True) +# opgrid = legacy.Runner(theory, operator_card, path=path).op_grid +# opg = opgrid.compute() +# theory.xif = 1.0 + epsilon +# path.unlink(missing_ok=True) +# sv_opgrid = legacy.Runner(theory, operator_card, path=path).op_grid +# sv_opg = sv_opgrid.compute() +# np.testing.assert_allclose( +# opg[(9, 4)]["operator"], sv_opg[(9, 4)]["operator"], atol=2.5 * epsilon +# )