Source code for mantispy.io._jump

"""JUMP Cell Painting profiles and their perturbation annotation.

A JUMP plate parquet carries three metadata columns (source, plate, well) and 4762
features. The compound or gene each well received is recorded in a separate repository,
keyed by ``Metadata_JCP2022``. This module reads the profiles with
:func:`~mantispy.io.read_profiles` and joins the annotation onto them.

Sources, all public over HTTPS:

* profiles: Cell Painting Gallery, accession ``cpg0016-jump``
* annotation: ``jump-cellpainting/datasets`` on GitHub

References:
    Chandrasekaran et al. (2024) Nature Methods 21:1114, the JUMP Cell Painting datasets.
"""

from __future__ import annotations

from collections.abc import Sequence
from pathlib import Path

import pandas as pd
from anndata import AnnData

from mantispy._core._utils import get_logger
from mantispy.io._profiles import read_profiles

#: JUMP's annotation tables, pinned by sha256 in the dataset registry because the upstream repository is mutable.
#: A changed table fails the checksum instead of changing the annotation.
TABLES = ("plate", "well", "compound")

#: JUMP's negative control: DMSO, under its JCP identifier.
NEGATIVE_CONTROL = "JCP2022_033924"

#: Kinds of perturbation the annotation covers, and the table each is described by.
KINDS = ("compound",)

_JOIN_ON = ["Metadata_Source", "Metadata_Plate", "Metadata_Well"]


def jump_metadata(name: str) -> pd.DataFrame:
    """Read one of JUMP's annotation tables, ``"plate"``, ``"well"`` or ``"compound"``."""
    if name not in TABLES:
        raise ValueError(f"name must be one of {TABLES}, got {name!r}")
    from mantispy.ds._datasets import _files

    (path,) = _files("_jump_annotation", select=lambda file_name: file_name == f"jump_{name}.csv.gz")
    return pd.read_csv(path)


[docs] def read_jump(paths: str | Path | Sequence[str | Path], annotate: bool = True, **kwargs) -> AnnData: """Read JUMP plate profiles, optionally joining the annotation. Args: paths: One or more ``{plate}.parquet`` files in the Cell Painting Gallery layout. annotate: Join the well and compound tables, which map the three metadata columns to a perturbation. Downloads about 14 MB once and caches it. kwargs: Passed to :func:`~mantispy.io.read_profiles`. ``on_column_mismatch="intersect"`` is useful when plates come from different sources. Returns: An :class:`~anndata.AnnData` at well resolution. With ``annotate`` it carries ``Metadata_JCP2022``, ``Metadata_Perturbation``, ``Metadata_InChIKey`` and ``Metadata_Control``. Notes: JUMP plates from different sources share their feature names but not always the same set of features; pass ``on_column_mismatch="intersect"`` when mixing sources. """ adata = read_profiles(paths, resolution="well", **kwargs) if annotate: from mantispy.pp._annotate import annotate_jump annotate_jump(adata) return adata
def join_jump_annotation(obs: pd.DataFrame, kind: str = "compound") -> pd.DataFrame: """Join the JUMP annotation onto an ``obs`` frame keyed by source, plate and well.""" if kind not in KINDS: raise ValueError(f"kind must be one of {KINDS}, got {kind!r}") missing = [column for column in _JOIN_ON if column not in obs] if missing: raise KeyError( f"obs is missing {missing}, the columns the JUMP annotation is keyed by. Read the plate " "parquet with mt.io.read_jump to get them." ) wells = jump_metadata("well") for frame in (obs, wells): for column in _JOIN_ON: frame[column] = frame[column].astype(str) joined = obs.merge(wells, on=_JOIN_ON, how="left", validate="m:1") unannotated = int(joined["Metadata_JCP2022"].isna().sum()) if unannotated: get_logger().warning( "%d of %d wells have no JUMP annotation; their Metadata_JCP2022 is missing", unannotated, len(joined) ) compounds = jump_metadata("compound")[["Metadata_JCP2022", "Metadata_InChIKey"]] joined = joined.merge(compounds, on="Metadata_JCP2022", how="left", validate="m:1") joined["Metadata_Perturbation"] = joined["Metadata_JCP2022"].astype(str) joined["Metadata_Control"] = (joined["Metadata_JCP2022"] == NEGATIVE_CONTROL).to_numpy() return joined