import contextlib
import os
from abc import ABC, abstractmethod
from argparse import ArgumentParser
from pathlib import Path
from typing import Sequence, Iterable
import lazycon
from ..io import save, PathLike, load
class Layout(ABC):
@abstractmethod
def build(self, *args, **kwargs):
"""Build a new experiment."""
@abstractmethod
def run(self, *args, **kwargs):
"""Run a built experiment."""
@abstractmethod
def build_parser(self, parser: ArgumentParser):
"""Appropriately updates a console arguments parser for the `build` method."""
@abstractmethod
def run_parser(self, parser: ArgumentParser):
"""Appropriately updates a console arguments parser for the `run` method."""
@contextlib.contextmanager
def change_current_dir(folder: PathLike):
current = os.getcwd()
try:
os.chdir(folder)
yield
finally:
os.chdir(current)
[docs]class Flat(Layout):
"""
Generates an experiment with a 'flat' structure.
Creates a subdirectory of ``experiment_path`` for the each entry of ``split``.
The subdirectory contains corresponding structure of identifiers.
Also, the config file from ``config_path`` is copied to ``experiment_path/resources.config``.
Parameters
----------
split
an iterable with groups of ids.
prefixes: Sequence[str]
the corresponding prefixes for each identifier group of ``split``
which will be used to generate appropriate filenames.
Default is ``('train', 'val', 'test')``.
Examples
--------
>>> ids = [
>>> [[1, 2, 3], [4, 5, 6], [7, 8]],
>>> [[1, 4, 8], [7, 5, 2], [6, 3]],
>>> ]
>>> Flat(ids).build('some_path.config', 'experiments/base')
# resulting folder structure:
# experiments/base:
# - resources.config
# - experiment_0:
# - train_ids.json # 1, 2, 3
# - val_ids.json # 4, 5, 6
# - test_ids.json # 7, 8
# - experiment_1:
# - train_ids.json # 1, 4, 8
# - val_ids.json # 7, 5, 2
# - test_ids.json # 6, 3
"""
def __init__(self, split: Iterable[Sequence], prefixes: Sequence[str] = ('train', 'val', 'test')):
self.prefixes = prefixes
self.split = list(split)
@staticmethod
def _expand_prefix(prefix):
return f'{prefix}_ids.json'
def build(self, config: PathLike, folder: PathLike, keep: Sequence[str] = None):
folder = Path(folder)
for i, ids in enumerate(self.split):
# TODO: move check to constructor
if len(ids) != len(self.prefixes):
raise ValueError(f"The number of identifier groups ({len(ids)}) "
f"does not match the number of prefixes ({len(self.prefixes)})")
local = folder / f'experiment_{i}'
local.mkdir(parents=True)
for val, prefix in zip(ids, self.prefixes):
save(val, local / self._expand_prefix(prefix), indent=0)
# resource manager is needed here, because there may be inheritance
lazycon.load(config).dump(folder / 'resources.config', keep)
def run(self, config: PathLike, folds: Sequence[int] = None):
root = Path(config).parent
if folds is None:
folds = range(len(self.split))
for fold in folds:
with change_current_dir(root / f'experiment_{fold}'):
lazycon.load(config).get('run_experiment')
def build_parser(self, parser: ArgumentParser):
parser.add_argument('folder', help='Destination folder.')
parser.add_argument('--keep', nargs='+', default=None, help='The definitions to keep. By default - all.')
def run_parser(self, parser: ArgumentParser):
parser.add_argument('-f', '--folds', nargs='+', help='Folds to run.')
def get_ids(self, prefix, folder='.'):
assert prefix in self.prefixes
return load(Path(folder) / self._expand_prefix(prefix))
def __getattr__(self, prefix):
return self.get_ids(prefix, '.')