Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Items without prefix refer to a global change.

## [Unreleased](https://github.com/NNPDF/eko/compare/v0.15.6...HEAD)

### Added
- py: Read metadata, theory and operator cards from an EKO archive without extracting operators ([#563](https://github.com/NNPDF/eko/pull/563))

## [0.15.6](https://github.com/NNPDF/eko/compare/v0.15.5...v0.15.6) - 2026-09-08

### Added
Expand Down
18 changes: 10 additions & 8 deletions src/eko/io/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,21 @@ def load(cls, path: os.PathLike):
paths = InternalPaths(path)
# read raw file first to catch version
raw = yaml.safe_load(paths.metadata.read_text(encoding="utf-8"))
content = cls.from_raw(raw)
content._path = path
return content

@classmethod
def from_raw(cls, raw: dict) -> "Metadata":
Comment thread
lol782 marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
def from_raw(cls, raw: dict) -> "Metadata":
def from_raw(cls, raw: dict) -> Self:

we can use Self here since a) we are already requiring py3.11 and b) we return an instance of cls and not Metadata explicitly (see also PEP 673).

change here and also runcards.py

"""Build metadata from raw yaml, applying legacy patches."""
version = parse(raw["version"])
data_version = int(raw["data_version"])
# patch if necessary
if data_version == 1:
if version.major == 0 and version.minor == 13:
raw = v1.update_metadata(paths, raw)
raw = v1.update_metadata(raw)
elif version.major == 0 and version.minor == 14:
raw = v2.update_metadata(paths, raw)

# now we are ready
content = cls.from_dict(raw)
content._path = path
return content
raw = v2.update_metadata(raw)
return cls.from_dict(raw)

def update(self):
"""Update the disk copy of metadata."""
Expand Down
60 changes: 60 additions & 0 deletions src/eko/io/runcards.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
squared value, for consistency. Squares are consistently taken inside.
"""

import pathlib
import tarfile
from dataclasses import dataclass
from math import nan
from typing import List, Optional, Union

import numpy as np
import numpy.typing as npt
import yaml

from .. import basis_rotation as br
from .. import interpolation, msbar_masses
Expand All @@ -19,7 +22,10 @@
from ..quantities import heavy_quarks as hq
from ..quantities.couplings import CouplingsInfo
from ..quantities.heavy_quarks import HeavyInfo, QuarkMassScheme
from . import v1, v2
from .dictlike import DictLike
from .metadata import Metadata
from .paths import METADATAFILE, OPERATORFILE, THEORYFILE
from .types import (
EvolutionMethod,
InversionMethod,
Expand Down Expand Up @@ -78,6 +84,15 @@ def __post_init__(self):
if self.matching_order is None:
self.matching_order = (self.order[0] - 1, 0)

@classmethod
def from_raw(cls, raw: dict, data_version: int) -> "TheoryCard":
"""Build a theory card from a raw dictionary."""
if data_version == 1:
raw = v1.update_theory(raw)
if data_version == 2:
raw = v2.update_theory(raw)
return cls.from_dict(raw)


@dataclass
class Debug(DictLike):
Expand Down Expand Up @@ -170,6 +185,15 @@ def pids(self):
"""Internal flavor basis, used for computation."""
return np.array(br.flavor_basis_pids)

@classmethod
def from_raw(cls, raw: dict, data_version: int, theory_raw) -> "OperatorCard":
"""Build an operator card from a raw dictionary."""
if data_version == 1:
raw = v1.update_operator(raw, theory_raw)
if data_version == 2:
raw = v2.update_operator(raw, theory_raw)
return cls.from_dict(raw)


Card = Union[TheoryCard, OperatorCard]

Expand Down Expand Up @@ -343,3 +367,39 @@ def masses(theory: TheoryCard, evmeth: EvolutionMethod) -> List[SquaredScale]:
return [mq.value**2 for mq in theory.heavy.masses]

raise ValueError(f"Unknown mass scheme '{theory.heavy.masses_scheme}'")


def _read_yaml_members(tar, filenames) -> dict[str, dict]:
"""Extract and parse several yaml files in a single pass."""
wanted = set(filenames)
found = {}
for member in tar.getmembers():
Comment thread
lol782 marked this conversation as resolved.
name = pathlib.Path(member.name).name
if name in wanted:
extracted = tar.extractfile(member)
if extracted is not None:
found[name] = yaml.safe_load(extracted.read())
missing = wanted - found.keys()
if missing:
raise KeyError(f"{missing} not found in {tar.name}")
return found


def read_eko_cards(
eko_path: pathlib.Path | str,
) -> tuple[Metadata, TheoryCard, OperatorCard]:
"""Read metadata, theory and operator cards from an EKO archive.

The (large) operators are never extracted, only the small yaml files.
"""
with tarfile.open(eko_path) as tar:
raw_files = _read_yaml_members(tar, [METADATAFILE, THEORYFILE, OPERATORFILE])

raw_meta = raw_files[METADATAFILE]
raw_theory = raw_files[THEORYFILE]
raw_operator = raw_files[OPERATORFILE]
metadata = Metadata.from_raw(raw_meta)
data_version = metadata.data_version
operator = OperatorCard.from_raw(raw_operator, data_version, raw_theory)
theory = TheoryCard.from_raw(raw_theory, data_version)
return metadata, theory, operator
19 changes: 4 additions & 15 deletions src/eko/io/struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import yaml

from .. import interpolation
from . import exceptions, raw, v1, v2
from . import exceptions, raw
from .access import AccessConfigs
from .inventory import Inventory
from .items import Evolution, Matching, Operator, Recipe, Target
Expand Down Expand Up @@ -133,25 +133,14 @@ def evolgrid(self) -> List[EPoint]:
def theory_card(self):
"""Provide theory card, retrieving from the dump."""
raw_th = yaml.safe_load(self.paths.theory_card.read_text(encoding="utf-8"))
if self.metadata.data_version in [1]:
raw_th = v1.update_theory(raw_th)
if self.metadata.data_version in [2]:
raw_th = v2.update_theory(raw_th)
return TheoryCard.from_dict(raw_th)
return TheoryCard.from_raw(raw_th, self.metadata.data_version)

@property
def operator_card(self):
"""Provide operator card, retrieving from the dump."""
raw_op = yaml.safe_load(self.paths.operator_card.read_text(encoding="utf-8"))
if self.metadata.data_version in [1]:
# here we need to read also the theory card
raw_th = yaml.safe_load(self.paths.theory_card.read_text(encoding="utf-8"))
raw_op = v1.update_operator(raw_op, raw_th)
if self.metadata.data_version in [2]:
# here we need to read also the theory card
raw_th = yaml.safe_load(self.paths.theory_card.read_text(encoding="utf-8"))
raw_op = v2.update_operator(raw_op, raw_th)
return OperatorCard.from_dict(raw_op)
raw_th = yaml.safe_load(self.paths.theory_card.read_text(encoding="utf-8"))
return OperatorCard.from_raw(raw_op, self.metadata.data_version, raw_th)

# persistency control
# -------------------
Expand Down
4 changes: 1 addition & 3 deletions src/eko/io/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
that API version.
"""

from .paths import InternalPaths


def update_metadata(paths: InternalPaths, raw: dict) -> dict:
def update_metadata(raw: dict) -> dict:
"""Modify the raw metadata to the new format.

Parameters
Expand Down
4 changes: 1 addition & 3 deletions src/eko/io/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
exactly that API version.
"""

from .paths import InternalPaths


def update_metadata(paths: InternalPaths, raw: dict) -> dict:
def update_metadata(raw: dict) -> dict:
"""Modify the raw metadata to the new format.

Parameters
Expand Down
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ def operator_card():
return card


@pytest.fixture
def eko_test_paths():
data_dir = pathlib.Path(__file__).parent / "data"
return [
data_dir / "v1-0.13.tar",
data_dir / "v1-0.14.tar",
data_dir / "v3.tar",
]


class EKOFactory:
def __init__(self, theory: TheoryCard, operator: OperatorCard, path: os.PathLike):
self.path = path
Expand Down
11 changes: 3 additions & 8 deletions tests/eko/io/test_legacy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import pathlib

import numpy as np
import numpy.testing
from banana import toy
Expand All @@ -9,18 +7,15 @@
from ekobox.apply import apply_pdf
from ekomark.benchmark.external import LHA_utils

TEST_DATA_DIR = (
pathlib.Path(__file__).parents[2] / "data"
) # directory of the EKO object
pdf = toy.mkPDF("", 0)

x_grid = LHA_utils.toy_xgrid
EP = (10000.0, 4)


def test_read_legacy():
for name in ["v1-0.13.tar", "v1-0.14.tar", "v3.tar"]:
with eko.EKO.read(TEST_DATA_DIR / name) as evolution_operator:
def test_read_legacy(eko_test_paths):
for path in eko_test_paths:
with eko.EKO.read(path) as evolution_operator:
# Check the cards
assert isinstance(
evolution_operator.theory_card, eko.io.runcards.TheoryCard
Expand Down
11 changes: 11 additions & 0 deletions tests/eko/io/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from eko import EKO, interpolation
from eko.io import struct
from eko.io.items import Target
from eko.io.metadata import Metadata
from eko.io.runcards import OperatorCard, TheoryCard, read_eko_cards
from tests.conftest import EKOFactory


Expand Down Expand Up @@ -197,6 +199,15 @@ def test_load_opened(self, tmp_path: pathlib.Path, eko_factory: EKOFactory):

assert read_closed.metadata == read_opened.metadata

def test_read_eko_cards(self, eko_test_paths):
"""Load metadata and both YAML cards from legacy EKO archives."""
for path in eko_test_paths:
metadata, theory, operator = read_eko_cards(path)

assert isinstance(metadata, Metadata)
assert isinstance(theory, TheoryCard)
assert isinstance(operator, OperatorCard)

def test_version(self, tmp_path: pathlib.Path, eko_factory: EKOFactory):
"""Test asserted version. Should either be supported version, or have a postrelease addition"""
eko = eko_factory.get()
Expand Down