Source code for mantispy.io._plate

from __future__ import annotations

from collections.abc import Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Literal

from mantispy.io._export import export_plate_dirs, is_export_plate_dir, read_cellprofiler_export
from mantispy.io._gallery import read_gallery_plate

if TYPE_CHECKING:
    from spatialdata import SpatialData

Layout = Literal["gallery", "cellprofiler"]


def _detect_layout(path: Path) -> Layout:
    """Tell a Cell Painting Gallery source from a CellProfiler export by what sits under `path`.

    A gallery source is the one with a ``workspace/`` tree, and that is checked first: a source that has been
    read once also holds SpatialData zarr stores, which carry a ``tables/`` directory like an export does.
    An export plate folder has its table in ``tables/`` and is looked for at `path` and one level below it.
    """
    if (path / "workspace").is_dir():
        return "gallery"
    if is_export_plate_dir(path) or export_plate_dirs(path):
        return "cellprofiler"
    msg = (
        f"{path} is neither a Cell Painting Gallery source, which holds a 'workspace/' tree, nor a "
        "CellProfiler export, which holds a table under 'tables/' per plate. Pass layout= to say which it is."
    )
    raise ValueError(msg)


def _export_plate_dir(root: Path, plate: str | None) -> Path:
    if is_export_plate_dir(root):
        if plate is not None and root.name != plate:
            msg = f"{root} is the plate folder of {root.name!r}, not of {plate!r}"
            raise ValueError(msg)
        return root
    folders = export_plate_dirs(root)
    if plate is not None:
        if (folder := root / plate) in folders:
            return folder
        msg = f"no plate {plate!r} under {root}; found {[path.name for path in folders]}"
        raise FileNotFoundError(msg)
    if len(folders) > 1:
        msg = f"{root} holds several plates; pass plate= to choose one of {[path.name for path in folders]}"
        raise ValueError(msg)
    return folders[0]


[docs] def read_plate( path: Path | str, plate: str | None = None, *, batch: str | None = None, layout: Layout | None = None, wells: Sequence[str] | None = None, plane: int | None = None, profile: str | Path | None = "normalized_feature_select_negcon_batch", plate_format: int = 384, lazy: bool = True, ) -> SpatialData: """Read one plate of images, segmentations and measurements into a :class:`~spatialdata.SpatialData` object. Two layouts are read, told apart by what sits under `path` unless `layout` says which it is. A **Cell Painting Gallery** source (``layout="gallery"``, needs `batch` and `plate`) gives fields of view as Images, the CellProfiler Nuclei, Cells and Cytoplasm segmentations as Labels, the wells as Shapes, and the ``wells`` and ``cells`` Tables. The gallery publishes one-pixel outlines, not masks, so the Labels are reconstructed and only unambiguous objects survive. Element names carry the plate barcode, so two plates concatenate without renaming. A **CellProfiler export** (``layout="cellprofiler"``) is a folder from the ``ExportForSpatialData`` module and is read from the manifest in its table's ``uns``, so nothing is reconstructed and a folder that was moved still reads. Where a source recorded stage coordinates and a pixel size, every element sits in three coordinate systems, ``{plate}_{well}_s{site}``, ``{plate}_{well}`` and ``{plate}``. Where it did not, each field sits in its own frame. See :doc:`the tutorial </tutorials/reading_plates>` for what each layout publishes and what is dropped. Args: path: A Cell Painting Gallery source directory, or an export root or one of its plate folders. plate: Plate barcode. Required for a gallery source; for an export root it picks one of the plate folders, and may be left out when the root holds one. batch: Batch name, the directory below ``images/`` and ``workspace/analysis/``. Gallery sources only. layout: Which layout `path` holds, detected from `path` when left out. wells: Wells to read images and labels for. Defaults to every well whose images are present under `path`, so a partial download reads back as itself. The well table always covers the whole plate. Gallery sources only. plane: Which ``Metadata_PlaneID`` to read where a source imaged a z stack. Required in that case: no plane is preferable to another, and picking one silently would hide the rest. Gallery sources only. profile: Variant of the well-level profile, read as ``workspace/profiles/{batch}/{plate}/{plate}_{profile}.csv.gz``. Sources that publish the profile under another name, ``{plate}.parquet`` among them, take a :class:`~pathlib.Path` instead. Pass ``None`` to leave out the well table and the well shapes. Gallery sources only. plate_format: Number of wells on the plate, used to place the wells on their nominal grid. Gallery sources only. lazy: Read arrays through dask instead of loading them into memory. Exports only. Returns: The plate. A table or element group is left out when nothing it would hold was read. Raises: ValueError: The layout cannot be told from `path`, an argument does not apply to the layout that was read, the plate mixes pixel sizes, ``load_data.csv`` names its images in an unknown way, or an image does not sit in the gallery layout. FileNotFoundError: ``load_data.csv``, the requested profile, or the named plate folder is missing. Examples: Read one plate of a gallery source, images and all: >>> import mantispy as mt >>> sdata = mt.io.read_plate( # doctest: +SKIP ... "cpg0000-jump-pilot/source_4", ... "BR00116991", ... batch="2020_11_04_CPJUMP1", ... ) Read one well of a plate a source published without a profile of the usual name: >>> sdata = mt.io.read_plate( # doctest: +SKIP ... "cpg0016-jump/source_1", ... "UL001641", ... batch="Batch1_20221004", ... wells=["A01"], ... profile=Path("UL001641.parquet"), ... plate_format=1536, ... ) Read a plate straight out of a pipeline run: >>> sdata = mt.io.read_plate("run_export/Plate1") # doctest: +SKIP """ path = Path(path) layout = layout or _detect_layout(path) if layout == "cellprofiler": if unsupported := [ name for name, value in (("batch", batch), ("wells", wells), ("plane", plane)) if value is not None ]: msg = f"{unsupported} do not apply to a CellProfiler export" raise ValueError(msg) return read_cellprofiler_export(_export_plate_dir(path, plate), lazy=lazy) if batch is None or plate is None: msg = "a Cell Painting Gallery source needs both a plate and a batch" raise ValueError(msg) return read_gallery_plate( path, batch, plate, wells=wells, plane=plane, profile=profile, plate_format=plate_format, )