Source code for spacr.qt.screens.measure

"""Illumination, AnnData and motility workflows integrated with Measure.

Measure applies illumination correction through
:func:`spacr.illumination.prepare_illumination_correction`. The integrated
Illumination page can estimate and assess a correction field independently
before a full measurement run. AnnData Export provides a typed settings form
for writing measurement tables as ``.h5ad`` files. Motility Assay quantifies
objects in existing time-series masks and writes per-object and per-track
measurements.

Each workflow opens as a complete page beside the Measure settings and retains
its headless entry point. Shared page and signal integration is implemented by
:mod:`spacr.qt.screens.map_barcodes`.
"""

from __future__ import annotations

import logging
from typing import Callable, Dict, Optional, Tuple

from PySide6.QtWidgets import QWidget

from ..widgets.fold_strip import FoldStrip, mark_folded_categories
from .map_barcodes import build_settings_screen, install_fold_strip

LOG = logging.getLogger(__name__)

#: Registry key of the screen this module hangs its strip on.
HOST_KEY = "measure"

#: Registry keys of the modules folded into it, in the order the strip
#: draws them -- which is the order a plate goes through them: the field
#: is estimated once and divided out of the pixels at every later enabled
#: stage -- segmentation today as well as measurement, which is why this
#: text names no single consumer -- and the AnnData file is written from
#: the tables afterwards.
#:
#: ILLUMINATION KEEPS ITS OWN FORM as well as its settings category here,
#: because the two ask different questions. The category is "correct these
#: fields while measuring them"; the button is "estimate the field and show
#: me the QC before I commit a day to the measure run". Both end in
#: ``prepare_illumination_correction``, so there is one implementation and
#: one set of keys behind the two doors.
#:
#: THE MOTILITY ASSAY IS A MEASUREMENT. It reads finished masks from
#: ``merged/*.npy``, builds per-cell rows, writes them to
#: ``measurements/measurements.db`` and adds per-track velocities -- which
#: is this host's job description with a time axis. It does not make
#: masks, so Mask Generation was the wrong home for it however much its
#: settings looked like tracking's.
#:
#: It opens its own screen rather than becoming a settings category here,
#: and that is not the fold rule being ignored. Its module runs on a
#: folder that has ALREADY been segmented, and Measure's own run has no
#: gate that would fire it -- so there is no seam to reveal, and inventing
#: a pipeline path would be a bigger change than the fold.
FOLDED_APPS: Tuple[str, ...] = ("illumination", "anndata_export", "motility")


#: What the tiles these three folds replaced said, kept so the buttons on
#: this masthead survive the loss of their registry rows.
#:
#: A folded module has no row, and the registry answers a key it does not
#: hold the same way it answers a typo: no name, no sentence, and "stable"
#: for the maturity. Illumination and AnnData Export were both assessed as
#: beta, so without this the two buttons would promise finished code and
#: read as "Illumination" and "Anndata Export" -- the key title-cased,
#: which is not how either module spells itself.
FOLD_FALLBACK = {
    "illumination": (
        "Illumination",
        "Estimate the flat-field from the plate itself and divide it out "
        "of the pixels at every enabled stage",
        "beta"),
    "anndata_export": (
        "AnnData Export",
        "Write the measured objects out as an AnnData object for "
        "single-cell analysis downstream",
        "beta"),
    "motility": (
        "Motility Assay",
        "Automated motility assay: track velocity + infection QC",
        "beta"),
}


#: ``key -> the categories on THIS screen's OWN form that are its settings``.
#:
#: THE ICON GOES WHERE THE SETTINGS ARE. Illumination's other half is not a
#: page and not a mounted card: the nine ``illumination_*`` keys have been
#: one of Measure's own categories for as long as ``measure_crop`` has
#: corrected fields. So the module has a button on this masthead carrying
#: its icon and a group of settings further down carrying nothing, and a
#: user who pressed the button has no way to see that the heading below is
#: the same module. The mark on the heading is what says so.
#:
#: AnnData Export and the Motility Assay are not here, and that is not an
#: omission: neither has a settings category on this form. They arrive as
#: PAGES, and a page is already marked with its module's icon on its tab --
#: see :func:`spacr.qt.screens.map_barcodes.show_as_page`.
FOLD_CATEGORIES: Dict[str, Tuple[str, ...]] = {
    "illumination": ("Illumination Correction",),
}


def _build_illumination(host_window: Optional[QWidget]) -> QWidget:
    """Illumination Correction's own screen: estimate, QC and save a field.

    The settings-driven module, unchanged, so the Run button here runs the
    same ``prepare_illumination_correction`` a measure run calls -- and
    running it alone is the capability Measure's own settings category has
    no way to ask for: the model and its QC figures are written, and a later
    measure run reuses them through ``illumination_model``.
    """
    return build_settings_screen("illumination", host_window)


def _build_anndata_export(host_window: Optional[QWidget]) -> QWidget:
    """AnnData Export's own screen: the settings-driven module, unchanged."""
    return build_settings_screen("anndata_export", host_window)


def _build_motility(host_window: Optional[QWidget]) -> QWidget:
    """The Motility Assay's own screen, run on masks that already exist."""
    return build_settings_screen("motility", host_window)


#: One builder per folded module — see
#: :func:`spacr.qt.screens.map_barcodes.install_fold_strip`.
BUILDERS: Dict[str, Callable[[Optional[QWidget]], QWidget]] = {
    "illumination": _build_illumination,
    "anndata_export": _build_anndata_export,
    "motility": _build_motility,
}


[docs] def mark_fold_sources(screen: QWidget) -> Dict[str, Tuple[str, ...]]: """Mark Measure categories with their folded module icons. :param screen: Host module screen. :returns: Mapping from folded application keys to marked category titles. """ if getattr(screen, "app_key", None) != HOST_KEY: return {} try: return mark_folded_categories( getattr(screen, "_settings_sections", ()) or (), FOLD_CATEGORIES) except Exception: LOG.debug("Could not mark %s's folded categories", HOST_KEY, exc_info=True) return {}
[docs] def install_folds(screen: QWidget) -> Optional[FoldStrip]: """Put Measure's fold strip on ``screen``'s masthead. :param screen: the host screen; nothing is installed unless its ``app_key`` is ``"measure"`` and it has a masthead. Its categories are then marked with the folded modules' icons. """ strip = install_fold_strip(screen, HOST_KEY, FOLDED_APPS, BUILDERS) mark_fold_sources(screen) return strip