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

Improve runner API #296

Merged
merged 16 commits into from
Aug 9, 2024
Merged
2 changes: 1 addition & 1 deletion extras/matching/check-matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def collect_data():
data = {}
pdf = toy.mkPDF("", 0)
for id in th_updates.keys():
with eko.EKO.open(f"./eko_{id}.tar") as evolution_operator:
with eko.EKO.read(f"./eko_{id}.tar") as evolution_operator:
x = evolution_operator.metadata.rotations.targetgrid.raw
data[id] = {
mu2: el["pdfs"]
Expand Down
2 changes: 1 addition & 1 deletion src/eko/io/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ClosedOperator(RuntimeError, exceptions.OutputError):
class AccessConfigs:
"""Configurations specified during opening of an EKO."""

path: Path
path: Optional[Path]
"""The path to the permanent object."""
readonly: bool
"Read-only flag"
Expand Down
2 changes: 1 addition & 1 deletion src/eko/io/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Metadata(DictLike):
"""
# tagging information
_path: Optional[pathlib.Path] = None
"""Path to temporary dir."""
"""Path to the open dir."""
version: str = vmod.__version__
"""Library version used to create the corresponding file."""
data_version: int = vmod.__data_version__
Expand Down
138 changes: 66 additions & 72 deletions src/eko/io/struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
import contextlib
import copy
import logging
import os
import pathlib
import shutil
import tarfile
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional

import numpy as np
Expand All @@ -30,7 +29,7 @@
TEMP_PREFIX = "eko-"


def inventories(path: pathlib.Path, access: AccessConfigs) -> dict:
def inventories(path: Path, access: AccessConfigs) -> dict:
"""Set up empty inventories for object initialization."""
paths = InternalPaths(path)
return dict(
Expand Down Expand Up @@ -293,7 +292,7 @@ def unload(self):
# operator management
# -------------------

def deepcopy(self, path: os.PathLike):
def deepcopy(self, path: Path):
"""Create a deep copy of current instance.

The managed on-disk object is copied as well, to the new ``path``
Expand Down Expand Up @@ -327,115 +326,109 @@ def deepcopy(self, path: os.PathLike):
self.unload()

new = copy.deepcopy(self)
new.access.path = pathlib.Path(path)
new.access.path = path
new.access.readonly = False
new.access.open = True

tmpdir = pathlib.Path(tempfile.mkdtemp(prefix=TEMP_PREFIX))
tmpdir = Path(tempfile.mkdtemp(prefix=TEMP_PREFIX))
new.metadata.path = tmpdir
# copy old dir to new dir
tmpdir.rmdir()
shutil.copytree(self.paths.root, new.paths.root)
new.close()

@staticmethod
def load(tarpath: os.PathLike, dest: os.PathLike):
"""Load the content of archive in a target directory.

Parameters
----------
tarpath: os.PathLike
the archive to extract
tmppath: os.PathLike
the destination directory

"""
def extract(tarpath: Path, dest: Path):
"""Extract the content of archive in a target directory."""
try:
with tarfile.open(tarpath) as tar:
raw.safe_extractall(tar, dest)
except tarfile.ReadError:
raise exceptions.OutputNotTar(f"Not a valid tar archive: '{tarpath}'")

@classmethod
def open(cls, path: os.PathLike, mode="r"):
"""Open EKO object in the specified mode."""
path = pathlib.Path(path)
access = AccessConfigs(path, readonly=False, open=True)
load = False
if mode == "r":
load = True
access.readonly = True
elif mode in "w":
pass
elif mode in "a":
load = True
else:
raise ValueError(f"Unknown file mode: {mode}")

tmpdir = pathlib.Path(tempfile.mkdtemp(prefix=TEMP_PREFIX))
if load:
cls.load(path, tmpdir)
metadata = Metadata.load(tmpdir)
opened = cls(
**inventories(tmpdir, access),
metadata=metadata,
access=access,
)
opened.operators.sync()
else:
opened = Builder(path=tmpdir, access=access)
def load(cls, path: Path):
"""Load the EKO from disk information.

return opened
Note
----
No archive path is assigned to the :cls:`EKO` object, setting its
:attr:`EKO.access.path` to `None`.
If you want to properly load from an archive, use the :meth:`read`
constructor.

"""
access = AccessConfigs(None, readonly=True, open=True)

metadata = Metadata.load(path)
loaded = cls(
**inventories(path, access),
metadata=metadata,
access=access,
)
loaded.operators.sync()

return loaded

@classmethod
def read(cls, path: os.PathLike):
"""Read the content of an EKO.
def read(
cls,
path: Path,
extract: bool = True,
dest: Optional[Path] = None,
readonly: bool = True,
):
felixhekhorn marked this conversation as resolved.
Show resolved Hide resolved
"""Load an existing EKO.

If the `extract` attribute is `True` the EKO is loaded from its archived
format. Otherwise, the `path` is interpreted as the location of an
already extracted folder.

Type-safe alias for::

EKO.open(... , "r")

"""
eko = cls.open(path, "r")
assert isinstance(eko, EKO)
return eko
if extract:
dir_ = Path(tempfile.mkdtemp(prefix=TEMP_PREFIX)) if dest is None else dest
cls.extract(path, dir_)
else:
dir_ = path

@classmethod
def create(cls, path: os.PathLike):
"""Create a new EKO.
loaded = cls.load(dir_)

Type-safe alias for::
loaded.access.readonly = readonly
if extract:
loaded.access.path = path

EKO.open(... , "w")
return loaded

"""
builder = cls.open(path, "w")
assert isinstance(builder, Builder)
@classmethod
def create(cls, path: Path):
"""Create a new EKO."""
access = AccessConfigs(path, readonly=False, open=True)
builder = Builder(
path=Path(tempfile.mkdtemp(prefix=TEMP_PREFIX)), access=access
)
return builder

@classmethod
def edit(cls, path: os.PathLike):
def edit(cls, *args, **kwargs):
"""Read from and write on existing EKO.

Type-safe alias for::

EKO.open(... , "a")
Alias of `EKO.read(..., readonly=False)`, see :meth:`read`.

"""
eko = cls.open(path, "a")
assert isinstance(eko, EKO)
return eko
return cls.read(*args, readonly=False, **kwargs)

def __enter__(self):
"""Allow EKO to be used in :obj:`with` statements."""
return self

def dump(self, archive: Optional[os.PathLike] = None):
def dump(self, archive: Optional[Path] = None):
"""Dump the current content to archive.

Parameters
----------
archive: os.PathLike or None
archive: Path or None
path to archive, in general you should keep the default, that will
make use of the registered path (default: ``None``)

Expand Down Expand Up @@ -474,7 +467,8 @@ def __exit__(self, exc_type: type, _exc_value, _traceback):
if exc_type is not None:
return

self.close()
if self.access.path is not None:
self.close()

@property
def raw(self) -> dict:
Expand All @@ -494,8 +488,8 @@ def raw(self) -> dict:
class Builder:
"""Build EKO instances."""

path: pathlib.Path
"""Path on disk to ."""
path: Path
"""Path on disk to the EKO."""
access: AccessConfigs
"""Access related configurations."""

Expand Down
13 changes: 5 additions & 8 deletions src/eko/runner/managed.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ..io.items import Evolution, Matching, Target
from ..io.runcards import OperatorCard, TheoryCard
from ..io.struct import EKO
from . import commons, operators, parts, recipes
from . import operators, parts, recipes


def solve(theory: TheoryCard, operator: OperatorCard, path: Path):
Expand All @@ -24,11 +24,7 @@ def solve(theory: TheoryCard, operator: OperatorCard, path: Path):

with EKO.create(path) as builder:
eko = builder.load_cards(theory, operator).build() # pylint: disable=E1101

atlas = commons.atlas(eko.theory_card, eko.operator_card)

recs = recipes.create(eko.operator_card.evolgrid, atlas)
eko.load_recipes(recs)
recipes.create(eko)

for recipe in eko.recipes:
assert isinstance(recipe, Evolution)
Expand All @@ -42,8 +38,9 @@ def solve(theory: TheoryCard, operator: OperatorCard, path: Path):
del eko.parts_matching[recipe]

for ep in operator.evolgrid:
headers = recipes.elements(ep, atlas)
parts_ = operators.retrieve(headers, eko.parts, eko.parts_matching)
parts_ = operators.retrieve(
operators.parts(ep, eko), eko.parts, eko.parts_matching
)
target = Target.from_ep(ep)
eko.operators[target] = operators.join(parts_)
# flush the memory
Expand Down
9 changes: 9 additions & 0 deletions src/eko/runner/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

from ..io.inventory import Inventory
from ..io.items import Evolution, Operator, Recipe
from ..io.struct import EKO
from ..io.types import EvolutionPoint
from . import commons, recipes


def retrieve(
Expand Down Expand Up @@ -91,3 +94,9 @@ def join(elements: List[Operator]) -> Operator:

"""
return reduce(dotop, reversed(elements))


def parts(ep: EvolutionPoint, eko: EKO) -> List[Recipe]:
"""Determine parts required for the given evolution point operator."""
atlas = commons.atlas(eko.theory_card, eko.operator_card)
return recipes._elements(ep, atlas)
15 changes: 12 additions & 3 deletions src/eko/runner/recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from typing import List

from ..io.items import Evolution, Matching, Recipe
from ..io.struct import EKO
from ..io.types import EvolutionPoint as EPoint
from ..matchings import Atlas, Segment
from . import commons


def elements(ep: EPoint, atlas: Atlas) -> List[Recipe]:
def _elements(ep: EPoint, atlas: Atlas) -> List[Recipe]:
"""Determine recipes to compute a given operator."""
recipes = []

Expand All @@ -26,10 +28,17 @@ def elements(ep: EPoint, atlas: Atlas) -> List[Recipe]:
return recipes


def create(evolgrid: List[EPoint], atlas: Atlas) -> List[Recipe]:
def _create(evolgrid: List[EPoint], atlas: Atlas) -> List[Recipe]:
"""Create all associated recipes."""
recipes = []
for ep in evolgrid:
recipes.extend(elements(ep, atlas))
recipes.extend(_elements(ep, atlas))

return list(set(recipes))


def create(eko: EKO):
"""Create and load all associated recipes."""
atlas = commons.atlas(eko.theory_card, eko.operator_card)
recs = _create(eko.operator_card.evolgrid, atlas)
eko.load_recipes(recs)
17 changes: 14 additions & 3 deletions tests/eko/io/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@ def test_new_error(self, tmp_path: pathlib.Path, theory_card, operator_card):
for args in [(None, None), (theory_card, None), (None, operator_card)]:
with pytest.raises(RuntimeError, match="missing"):
with struct.EKO.create(tmp_path / "Blub2.tar") as builder:
eko = builder.load_cards(*args).build()
_ = builder.load_cards(*args).build()

def test_load_error(self, tmp_path):
# try to read from a non-tar path
no_tar_path = tmp_path / "Blub.tar"
no_tar_path.write_text("Blub", encoding="utf-8")
with pytest.raises(ValueError, match="tar"):
struct.EKO.read(no_tar_path)
with pytest.raises(ValueError, match="file mode"):
struct.EKO.open(no_tar_path, "ü")

def test_properties(self, eko_factory: EKOFactory):
mu = 10.0
Expand Down Expand Up @@ -184,3 +182,16 @@ def test_context_operator(self, eko_factory: EKOFactory):
assert isinstance(op, struct.Operator)

assert eko.operators.cache[Target.from_ep(ep)] is None

def test_load_opened(self, tmp_path: pathlib.Path, eko_factory: EKOFactory):
"""Test the loading of an already opened EKO."""
eko = eko_factory.get()
eko.close()
# drop from cache to avoid double close by the fixture
eko_factory.cache = None

assert eko.access.path is not None
read_closed = EKO.read(eko.access.path, dest=tmp_path)
read_opened = EKO.read(tmp_path, extract=False)

assert read_closed.metadata == read_opened.metadata
2 changes: 1 addition & 1 deletion tests/eko/runner/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def identity(neweko: EKO):
@pytest.fixture
def ekoparts(neweko: EKO, identity: Operator):
atlas = commons.atlas(neweko.theory_card, neweko.operator_card)
neweko.load_recipes(recipes.create(neweko.operator_card.evolgrid, atlas))
neweko.load_recipes(recipes._create(neweko.operator_card.evolgrid, atlas))

for rec in neweko.recipes:
neweko.parts[rec] = identity
Expand Down
Loading
Loading