Source code for pyfli.io.save_direction

"""
Turn a ``save_config`` dictionary into a ready :class:`DataSaver` session.

This module belongs to :mod:`pyfli.io` and is part of PyFLI detector importers, file
readers, saving helpers, and processed-data loaders. Public API includes the class
:class:`SaveLoadDirector`, which centralizes the branching that decides -- from a
single config dict -- whether a session is saved, loaded, or skipped, and where its
folder lives.
"""

# pyfli/io/save_direction.py
import os
from typing import Any, ClassVar

from pyfli import logging

from .data_saving import DataSaver


[docs] class SaveLoadDirector: """ Resolve a ``save_config`` dictionary into a :class:`DataSaver` session. Instead of scattering ``if mode == ...`` / ``if auto_path ...`` checks through a script, collect every save/load decision in one dict and hand it to this class. :meth:`build` returns a writable session (``mode='save'``), reconnects to an existing one (``mode='load'``), or returns ``None`` (``mode=None``). Parameters ---------- config : dict | None User-supplied settings. Any key that is omitted falls back to :attr:`DEFAULTS`. Recognized keys: ``mode`` : {'save', 'load', None} ``'save'`` builds a writable session, ``'load'`` reconnects to an existing one, ``None`` disables saving. ``auto_path`` : bool If ``True``, the session folder is derived from ``DATA_PATH``. If ``False``, a manual path is required (see below). ``DATA_PATH`` : str | None Source data path; required when ``auto_path=True`` (for both save and load). ``manual_load_path`` : str | None Exact existing session folder to load; used when ``mode='load'`` and ``auto_path=False``. ``manual_save_root_path`` : str | None Root under which the session folder is created; required when ``mode='save'`` and ``auto_path=False`` (also accepted as a load root). ``bin_r`` : int Spatial binning radius; part of the derived ``save_suffix``. ``version`` : int Analysis version; part of the derived ``save_suffix``. ``new_session`` : bool Forwarded to :class:`DataSaver` when ``mode='save'``. ``MODEL_TYPE``, ``free_memory`` : carried through untouched for downstream consumers. **overrides : Any Convenience keyword overrides merged on top of ``config``. Attributes ---------- config : dict The fully merged configuration, including the derived ``save_suffix``. """ DEFAULTS: ClassVar[dict[str, Any]] = { "MODEL_TYPE": "bi-exponential", "bin_r": 0, "free_memory": True, "mode": "save", # 'save' | 'load' | None "auto_path": True, # True -> path auto-derived from DATA_PATH "manual_load_path": None, "manual_save_root_path": None, "DATA_PATH": None, "new_session": True, "version": 1, } VALID_MODES: ClassVar[tuple[str | None, ...]] = ("save", "load", None) def __init__(self, config: dict | None = None, **overrides: Any) -> None: cfg = {**self.DEFAULTS, **(config or {}), **overrides} if cfg["mode"] not in self.VALID_MODES: raise ValueError( f"save_config['mode'] must be one of {self.VALID_MODES}, " f"got {cfg['mode']!r}" ) cfg["save_suffix"] = f"bin{cfg['bin_r']}_pf_Analysis_v{cfg['version']}" self.config = cfg # ------------------------------------------------------------------ # dict-like access, so existing ``cfg[...]`` call sites keep working # ------------------------------------------------------------------ def __getitem__(self, key: str) -> Any: return self.config[key]
[docs] def get(self, key: str, default: Any = None) -> Any: """Return ``self.config[key]`` if present, else ``default``.""" return self.config.get(key, default)
[docs] def to_dict(self) -> dict: """Return a shallow copy of the merged configuration.""" return dict(self.config)
@property def save_suffix(self) -> str: """Folder-name suffix derived from ``bin_r`` and ``version``.""" return self.config["save_suffix"] @property def mode(self) -> str | None: """Configured session mode: ``'save'``, ``'load'``, or ``None``.""" return self.config["mode"] def __repr__(self) -> str: c = self.config return ( f"{type(self).__name__}(mode={c['mode']!r}, " f"auto_path={c['auto_path']}, save_suffix={c['save_suffix']!r})" ) # ------------------------------------------------------------------ # path resolution # ------------------------------------------------------------------ @staticmethod def _data_saving_folder(data_path: str) -> str: """ Base path that an auto-derived session hangs off of. A file path is used as-is; a directory path gets a trailing ``"_"`` appended to its basename so :class:`DataSaver` builds a sibling folder rather than nesting inside the data directory. """ if os.path.isfile(data_path): return data_path return os.path.join( os.path.dirname(data_path), f"{os.path.basename(data_path)}_" ) def _auto_target(self) -> str: """Auto-derived base path, or raise if ``DATA_PATH`` is unset.""" data_path = self.config["DATA_PATH"] if not data_path: raise ValueError( f"auto_path=True requires DATA_PATH to be set (mode={self.mode!r})." ) return self._data_saving_folder(data_path) # ------------------------------------------------------------------ # per-mode builders # ------------------------------------------------------------------ def _build_load(self) -> DataSaver: cfg = self.config if cfg["auto_path"]: load_target = DataSaver.resolve_path( self._auto_target(), cfg["save_suffix"] ) saver = DataSaver.load(load_target) logging.info( f'Data loaded (auto-derived path) - Session: "{saver.save_dir}"' ) return saver if cfg["manual_load_path"]: load_target = cfg["manual_load_path"] elif cfg["manual_save_root_path"]: load_target = DataSaver.resolve_path( cfg["manual_save_root_path"], cfg["save_suffix"] ) else: raise ValueError( "auto_path=False (mode='load') requires either 'manual_load_path' " "or 'manual_save_root_path' to be set." ) saver = DataSaver.load(load_target) logging.info(f'Data loaded - Session: "{saver.save_dir}"') return saver def _build_save(self) -> DataSaver: cfg = self.config if cfg["auto_path"]: saver = DataSaver( path=self._auto_target(), folder_name=cfg["save_suffix"], new_session=cfg["new_session"], ) logging.info( f'Data Saving (auto-derived path) - Session: "{saver.save_dir}"' ) return saver if not cfg["manual_save_root_path"]: raise ValueError( "auto_path=False (mode='save') requires 'manual_save_root_path' to " "be set -- no path to save the data was given." ) saver = DataSaver( path=cfg["manual_save_root_path"], folder_name=cfg["save_suffix"], new_session=cfg["new_session"], ) logging.info(f'Data Saving - Session: "{saver.save_dir}"') return saver def _build_none(self) -> None: """``mode=None``: saver disabled entirely.""" logging.info("No data will be saved in this session") return None # ------------------------------------------------------------------ # public entry point # ------------------------------------------------------------------
[docs] def build(self) -> DataSaver | None: """ Construct the :class:`DataSaver` described by the configuration. Returns ------- DataSaver | None A writable session (``mode='save'``), a reconnected session (``mode='load'``), or ``None`` (``mode=None``). """ dispatch = { "save": self._build_save, "load": self._build_load, None: self._build_none, } return dispatch[self.config["mode"]]()
[docs] @classmethod def from_config( cls, config: dict | None = None, **overrides: Any ) -> DataSaver | None: """ One-shot helper: ``SaveLoadDirector(config, **overrides).build()``. Returns ------- DataSaver | None Same as :meth:`build`. """ return cls(config, **overrides).build()