Source code for pulserver.proxy._designs

"""Designs of a design store, found from the header of an incoming stream."""

from __future__ import annotations

import json

__all__ = ["DESIGN_PARAMETER", "Design", "DesignCache"]

import re
import threading
from collections import OrderedDict
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import Any

from ..host._store import ID_DIGITS, MANIFEST
from ..mrd._metadata import user_parameter
from ..mrd._sequence import read_chain
from ._enrich import SequenceTable
from ._seqdesc import describe, message

#: Header user parameter naming the design a stream was played from: its
#: identifier, as ``GENERATED`` or ``IMPORTED`` replied it.
DESIGN_PARAMETER = "pulserver_design"

_ENTRY = "sequence.seq"
_IDENTIFIER = re.compile(rf"[0-9a-f]{{{ID_DIGITS}}}")

#: Designs a cache keeps tabulated.
_KEPT = 8


[docs] @dataclass(frozen=True) class Design: """A stored design, as the reconstruction side reads it. Attributes ---------- directory ``<store>/<id>/``. table The readouts of the design's sequence chain, in play order. prospective_motion Whether the scan asks to be corrected for motion while it plays, which is what sends its series to the one reconstruction that may publish a pose. """ directory: Path table: SequenceTable prospective_motion: bool = False @cached_property def description(self) -> str: """The MRD text message describing the chain to a simulation. See :func:`~pulserver.proxy._seqdesc.message`. Built on first use and kept with the design. """ return message( [ describe(seq, subsequence_index=index) for index, (_, seq) in enumerate(read_chain(self.directory / _ENTRY)) ] )
[docs] class DesignCache: """The designs of a store, the most recently read kept tabulated. Reading a design tabulates its sequence chain, which is the expensive part of accepting a series; concurrent streams of one design wait for the first to finish rather than tabulating it again. A design that is no longer among the most recently read is read again when next named. The store is read and never written. """ def __init__(self, store: Path | str) -> None: self.store = Path(store) self._lock = threading.Lock() self._building: dict[Path, threading.Lock] = {} self._designs: OrderedDict[Path, Design] = OrderedDict()
[docs] def locate(self, header: Any) -> Path: """Return the directory of the design a header names. The design is named by :data:`DESIGN_PARAMETER`, a ``userParameterString`` holding the identifier. Raises ------ ValueError If the header names no design, or names it by something that is not an identifier. FileNotFoundError If the store holds no such design. """ named = user_parameter(header, DESIGN_PARAMETER) if named in (None, ""): raise ValueError(f"the header carries no {DESIGN_PARAMETER}") design = str(named).strip().lower() if not _IDENTIFIER.fullmatch(design): raise ValueError( f"{DESIGN_PARAMETER}={named!r} is not a design identifier: " f"{ID_DIGITS} hexadecimal digits" ) directory = self.store / design if not (directory / MANIFEST).is_file(): raise FileNotFoundError(f"no design {design} in {self.store}") return directory
[docs] def read(self, directory: Path) -> Design: """Return a design directory read, tabulating its chain unless kept.""" directory = Path(directory) with self._lock: building = self._building.setdefault(directory, threading.Lock()) with building: with self._lock: if directory in self._designs: self._designs.move_to_end(directory) return self._designs[directory] design = Design( directory=directory, table=SequenceTable.read( directory / _ENTRY, fov_offset_m=_fov_offset_m(directory) ), prospective_motion=_asks_for_motion_correction(directory / _ENTRY), ) with self._lock: self._designs[directory] = design while len(self._designs) > _KEPT: forgotten, _ = self._designs.popitem(last=False) self._building.pop(forgotten, None) return design
[docs] def resolve(self, header: Any) -> Design: """Return the design a header names; see :meth:`locate` and :meth:`read`.""" return self.read(self.locate(header))
def _fov_offset_m(directory: Path) -> tuple[float, float, float]: """Return the field-of-view shift a design was converted at, in m.""" manifest = json.loads((directory / MANIFEST).read_text()) return tuple( 1e-3 * float(v) for v in manifest.get("fov_offset_mm", (0.0, 0.0, 0.0)) ) def _asks_for_motion_correction(entry: Path) -> bool: """Whether a sequence file asks to be corrected for motion while it plays. Read from the preamble rather than the cache: a design is resolved before anything has been converted. """ try: with entry.open("r", errors="replace") as f: inside = False for line in f: stripped = line.strip() if stripped.startswith("["): if inside: return False inside = stripped.startswith("[DEFINITIONS]") continue if inside and stripped.startswith("EnablePmc"): parts = stripped.split() return len(parts) > 1 and parts[1] not in ("0", "0.0") except OSError: return False return False