Source code for itasc.contact_analysis.contacts.contact_labels
"""Contact cell-type labels — propagate a per-cell label onto contacts.
Each cell–cell contact (a row of the ``edges`` table) is labelled by the
unordered pair of its two cells' labels, supplied as a generic ``cell_id -> label``
map. A contact between cell types ``A`` and ``B`` is labelled ``"A-B"`` (sorted), so
a consumer can ask how subpopulations contact each other — homotypic vs
heterotypic — without that aggregation living here. The label map is the caller's
concern (e.g. a downstream, dataset-specific classification); this module is
label-agnostic.
Headless and Qt-free, like
:mod:`itasc.contact_analysis.contacts.signed_contact_length`: it operates on an
already-read
:class:`~itasc.contact_analysis.contacts.reader.PositionContactAnalysis`
plus a ``cell_id -> label`` map, so it never opens HDF5 itself and runs unchanged in
scripts, notebooks, and plugins.
"""
from __future__ import annotations
from collections.abc import Mapping
import numpy as np
from itasc.contact_analysis.contacts.reader import PositionContactAnalysis
#: Columns the label table carries, in order. Kept as a constant so the empty
#: table and the populated one cannot drift apart.
_COLUMNS = (
"frame",
"edge_id",
"cell_a",
"cell_b",
"label_a",
"label_b",
"contact_label",
"homotypic",
"fully_classified",
"length",
)
[docs]
def contact_label_for(
labels: Mapping[int, str],
cell_a: int,
cell_b: int,
*,
unclassified: str = "unclassified",
) -> str:
"""The sorted ``"label_a-label_b"`` contact label for one cell pair.
Each cell's label is looked up in *labels* (``cell_id -> label``); a cell
absent from the map takes *unclassified*. The pair is sorted so the contact
type is orientation-independent (``"A-B" == "B-A"``). This is the single
definition shared by :func:`label_contacts` (the per-edge table) and the
signed-contact-length reaction coordinate, so a contact types identically
wherever it is seen.
"""
label_a = labels.get(int(cell_a), unclassified)
label_b = labels.get(int(cell_b), unclassified)
return "-".join(sorted((label_a, label_b)))
[docs]
def label_contacts(
analysis: PositionContactAnalysis,
labels: Mapping[int, str],
*,
unclassified: str = "unclassified",
) -> dict[str, np.ndarray]:
"""Label every cell–cell contact by its two cells' NLS subpopulation labels.
For each ``kind == "cell_cell"`` edge, each endpoint's label is looked up in
*labels* (``cell_id -> label``; a cell absent from the map takes
*unclassified*) and the unordered-pair contact label
``"-".join(sorted([label_a, label_b]))`` is formed. The vocabulary is whatever
*labels* holds — nothing here is hard-wired to positive/negative.
Border edges (``kind == "border"``, ``cell_b == 0``) are not contacts between
two cells and are excluded. Fragments are **not** joined: a boundary split
across several edge rows yields several labelled rows that share the same
``(frame, cell_a, cell_b)`` and therefore the same label; ``edge_id`` and
``length`` are carried through so a consumer can join or length-weight later.
Columns (column-major, all equal length, one row per cell–cell edge):
* ``frame`` — the edge's frame.
* ``edge_id`` — the edge's id within its frame.
* ``cell_a`` / ``cell_b`` — the contacting cell ids (as stored, ``a < b``).
* ``label_a`` / ``label_b`` — each cell's NLS label, or *unclassified*.
* ``contact_label`` — sorted ``"label_a-label_b"`` pair.
* ``homotypic`` — ``label_a == label_b`` (True for two unclassified;
gate on ``fully_classified`` if that matters).
* ``fully_classified`` — both cells had a label in *labels*.
* ``length`` — the edge length, carried through for weighting.
Returns empty (but typed) arrays when there are no cell–cell edges.
"""
edges = analysis.edges
frame = np.asarray(edges.get("frame", ()), dtype=np.int64)
edge_id = np.asarray(edges.get("edge_id", ()), dtype=np.int64)
cell_a = np.asarray(edges.get("cell_a", ()), dtype=np.int64)
cell_b = np.asarray(edges.get("cell_b", ()), dtype=np.int64)
length = np.asarray(edges.get("length", ()), dtype=float)
kind = np.asarray(edges.get("kind", np.full(frame.shape, "cell_cell")), dtype=object)
out: dict[str, list] = {name: [] for name in _COLUMNS}
for i in range(frame.size):
if str(kind[i]) != "cell_cell":
continue
ca, cb = int(cell_a[i]), int(cell_b[i])
present_a, present_b = ca in labels, cb in labels
label_a = labels.get(ca, unclassified)
label_b = labels.get(cb, unclassified)
out["frame"].append(int(frame[i]))
out["edge_id"].append(int(edge_id[i]) if edge_id.size else 0)
out["cell_a"].append(ca)
out["cell_b"].append(cb)
out["label_a"].append(label_a)
out["label_b"].append(label_b)
out["contact_label"].append(contact_label_for(labels, ca, cb, unclassified=unclassified))
out["homotypic"].append(label_a == label_b)
out["fully_classified"].append(present_a and present_b)
out["length"].append(float(length[i]))
return {
"frame": np.asarray(out["frame"], dtype=np.int64),
"edge_id": np.asarray(out["edge_id"], dtype=np.int64),
"cell_a": np.asarray(out["cell_a"], dtype=np.int64),
"cell_b": np.asarray(out["cell_b"], dtype=np.int64),
"label_a": np.asarray(out["label_a"], dtype=object),
"label_b": np.asarray(out["label_b"], dtype=object),
"contact_label": np.asarray(out["contact_label"], dtype=object),
"homotypic": np.asarray(out["homotypic"], dtype=bool),
"fully_classified": np.asarray(out["fully_classified"], dtype=bool),
"length": np.asarray(out["length"], dtype=float),
}