Source code for eon.concorpus

"""Store path-based con blobs in a readcon-db corpus.

readcon still parses every frame. The corpus is a second copy: one
trajectory per distinct path plus blob, next to the run when ``config.ini``
is at most eight directories up, otherwise beside the con file. A missing
``readcon_db`` install leaves the con file as the only copy.

A dynamics movie can also ingest each directory of a simulation tree and
read those frames back with one read-only ``get_frame_texts`` call. The
``.con`` files remain the store.
"""

from __future__ import annotations

import gc
import logging
import os
import tempfile
from pathlib import Path

logger = logging.getLogger("eon.concorpus")

_FNV_OFFSET = 14695981039346656037
_FNV_PRIME = 1099511628211
_MASK = (1 << 64) - 1

_unavailable = False
_corpora: dict[str, object] = {}


[docs] def traj_id(path: str, text: str) -> int: """FNV-1a 64 over the absolute path, a NUL, and the con text.""" h = _FNV_OFFSET for byte in (path + "\0" + text).encode(): h ^= byte h = (h * _FNV_PRIME) & _MASK return h or 1
[docs] def corpus_dir(con_path: Path) -> Path: """Directory of the LMDB corpus for this con path.""" cur = con_path.resolve().parent for _ in range(8): if (cur / "config.ini").is_file(): return cur / "readcon.db" parent = cur.parent if parent == cur: break cur = parent return con_path.resolve().parent / "readcon.db"
def _loader_error() -> str: """dlopen text for a missing libreadcon_db.so, or an empty string.""" import ctypes try: ctypes.CDLL("libreadcon_db.so") except OSError as exc: return str(exc) return "" def _note_library_missing(exc: BaseException) -> None: """One warning. Later calls return before this runs.""" detail = _loader_error() or str(exc) logger.warning("libreadcon_db.so failed to load: %s", detail) def _corpus(directory: Path): global _unavailable if _unavailable: return None key = str(directory) cached = _corpora.get(key) if cached is not None: return cached try: from readcon_db import ConCorpus except (ImportError, OSError) as exc: _unavailable = True _note_library_missing(exc) return None try: db = ConCorpus(key) except Exception: logger.debug("readcon-db open failed for %s", key, exc_info=True) return None _corpora[key] = db return db
[docs] def mirror_con_text(path, text: str) -> None: """Insert one con blob. Identical path-and-text pairs are stored once.""" if not text or not text.strip(): return con_path = Path(path) try: resolved = str(con_path.resolve()) except OSError: resolved = str(con_path) db = _corpus(corpus_dir(con_path)) if db is None: return try: db.append_trajectory_str(traj_id(resolved, text), text, source=resolved) except Exception as exc: if "already exists" in str(exc): return logger.debug("readcon-db mirror skipped for %s: %s", resolved, exc)
[docs] def store_frame_text(path, text: str) -> tuple[int, int] | None: """Store one con blob. Returns ``(traj_id, 0)`` for a single frame. The corpus takes the con text only. A barrier is not an argument. A mode is not an argument either: it rides in the saddle frame as the displacements section. """ if not text or not text.strip(): return None con_path = Path(path) try: resolved = str(con_path.resolve()) except OSError: resolved = str(con_path) db = _corpus(corpus_dir(con_path)) if db is None: return None key = traj_id(resolved, text) try: db.append_trajectory_str(key, text, source=resolved) except Exception as exc: if "already exists" not in str(exc): logger.warning("readcon-db store failed for %s: %s", resolved, exc) return None return key, 0
[docs] def load_frame_text(directory: Path, traj: int, frame_idx: int) -> str | None: """Frame text for a key in this corpus, or None when it is absent.""" db = _corpus(Path(directory)) if db is None: return None try: return db.get_frame_text(int(traj), int(frame_idx)) except Exception: logger.warning( "readcon-db get_frame_text failed for %s frame %s:%s", directory, traj, frame_idx, exc_info=True, ) return None
[docs] def stored_frame_text(path) -> str | None: """Frame text for this path's current blob, or None when the corpus has no copy.""" con_path = Path(path) try: text = con_path.read_text() resolved = str(con_path.resolve()) except OSError: return None db = _corpus(corpus_dir(con_path)) if db is None: return None try: return db.get_frame_text(traj_id(resolved, text), 0) except Exception: return None
[docs] def mirror_con_path(path) -> None: """Insert the con file's current bytes.""" con_path = Path(path) try: text = con_path.read_text() except (OSError, UnicodeDecodeError): return mirror_con_text(con_path, text)
[docs] def directories_with_con(tree: Path) -> list[Path]: """Directories under ``tree`` that directly contain ``.con`` or ``.convel``.""" found: list[Path] = [] for dirpath, dirnames, filenames in os.walk(tree): dirnames.sort() if any(name.endswith(".con") or name.endswith(".convel") for name in filenames): found.append(Path(dirpath)) return found
def _index_tree(db, tree: Path) -> dict[str, tuple[int, int]]: index: dict[str, tuple[int, int]] = {} next_id = 1 for directory in directories_with_con(tree): rows = db.ingest_directory(str(directory), start_traj_id=next_id) for traj_id, _nframes, path in rows: index[str(Path(path).resolve())] = (int(traj_id), 0) next_id = max(next_id, int(traj_id) + 1) return index
[docs] def frame_texts_for_paths(tree, con_paths, corpus_cls=None) -> list[str]: """Ingest ``tree`` and return frame 0 text for each path, in order. ``corpus_cls`` defaults to ``readcon_db.ConCorpus``. Tests pass a stand-in with the same constructor, ``ingest_directory``, and ``get_frame_texts``. """ if corpus_cls is None: from readcon_db import ConCorpus corpus_cls = ConCorpus tree = Path(tree) with tempfile.TemporaryDirectory(prefix="eon-concorpus-") as corpus_dir: writer = corpus_cls(corpus_dir) index = _index_tree(writer, tree) # One LMDB environment per path. Drop the writer before the read-only open. close = getattr(writer, "close", None) if close is not None: close() del writer gc.collect() keys = [] for path in con_paths: resolved = str(Path(path).resolve()) if resolved not in index: raise FileNotFoundError(f"{path} was not ingested from {tree}") keys.append(index[resolved]) reader = corpus_cls(corpus_dir, readonly=True) return list(reader.get_frame_texts(keys))