Skip to content

Objects

The container, before any analysis touches it. Truecell holds one or more assays, the reductions computed off them, the neighbour graphs, the per-cell metadata and the command log — the same slots R's Seurat S4 class holds, as ordinary Python classes with __slots__.

The one structural difference worth knowing up front: R's Assay5 inherits from dgCMatrix; here Assay5 wraps a SciPy CSC matrix rather than subclassing it, because subclassing scipy.sparse is a well-known trap. Everything you reach through the generics behaves the same either way.

The object model is checked against Seurat anchor by anchor in The Object Model Itself — 91 of 91 exact, no tolerance.

The top-level object

Truecell

Truecell(assays: dict[str, AnyAssay], meta_data: DataFrame, active_assay: str, active_ident: Optional[Categorical] = None, graphs: Optional[dict[str, Graph]] = None, neighbors: Optional[dict[str, Neighbor]] = None, reductions: Optional[dict[str, DimReduc]] = None, images: Optional[dict[str, FOV]] = None, project_name: str = 'SeuratProject', misc: Optional[dict] = None, version: Optional[Version] = None, commands: Optional[list[TruecellCommand]] = None, tools: Optional[dict] = None)

Top-level Truecell single-cell data object.

Mirrors R's Seurat class from seurat.R.

Slots
  • assays (dict[str, AnyAssay])
  • meta_data (pd.DataFrame) — cells × metadata columns
  • active_assay (str)
  • active_ident (pd.Categorical)
  • graphs (dict[str, Graph])
  • neighbors (dict[str, Neighbor])
  • reductions (dict[str, DimReduc])
  • images (dict[str, FOV])
  • project_name (str)
  • misc (dict)
  • version (packaging.version.Version)
  • commands (list[TruecellCommand])
  • tools (dict)
Source code in truecell/truecell.py
def __init__(
    self,
    assays: dict[str, AnyAssay],
    meta_data: pd.DataFrame,
    active_assay: str,
    active_ident: Optional[pd.Categorical] = None,
    graphs: Optional[dict[str, Graph]] = None,
    neighbors: Optional[dict[str, Neighbor]] = None,
    reductions: Optional[dict[str, DimReduc]] = None,
    images: Optional[dict[str, FOV]] = None,
    project_name: str = "SeuratProject",
    misc: Optional[dict] = None,
    version: Optional[Version] = None,
    commands: Optional[list[TruecellCommand]] = None,
    tools: Optional[dict] = None,
) -> None:
    self.assays = assays
    self.meta_data = meta_data
    self.active_assay = active_assay
    self._active_ident = active_ident if active_ident is not None else pd.Categorical(
        meta_data.index.tolist()
    )
    self.graphs = graphs or {}
    self.neighbors = neighbors or {}
    self.reductions = reductions or {}
    self.images = images or {}
    self.project_name = project_name
    self.misc = misc or {}
    self.version = version or _VERSION
    self.commands = commands or []
    self.tools = tools or {}

image_names

image_names() -> list[str]

Names of the spatial images/FOVs (mirrors Images()).

Source code in truecell/truecell.py
def image_names(self) -> list[str]:
    """Names of the spatial images/FOVs (mirrors ``Images()``)."""
    return list(self.images)

get_tissue_coordinates

get_tissue_coordinates(image: Optional[str] = None) -> DataFrame

Centroid coordinates across images (mirrors GetTissueCoordinates).

Returns a DataFrame with columns x, y, cell, image.

Source code in truecell/truecell.py
def get_tissue_coordinates(self, image: Optional[str] = None) -> pd.DataFrame:
    """Centroid coordinates across images (mirrors ``GetTissueCoordinates``).

    Returns a DataFrame with columns ``x, y, cell, image``.
    """
    from .spatial.analysis import get_tissue_coordinates as _gtc
    return _gtc(self, image)

create_truecell_object

create_truecell_object(counts, assay: str = 'RNA', min_cells: int = 0, min_features: int = 0, project: str = 'SeuratProject', feature_names: Optional[list[str]] = None, cell_names: Optional[list[str]] = None, meta_data: Optional[DataFrame] = None, use_v5: bool = True) -> Truecell

Create a Truecell object from a counts matrix.

Mirrors R's CreateSeuratObject().

Parameters:

  • counts

    sparse or dense matrix (features × cells)

  • assay (str, default: 'RNA' ) –

    assay name (default "RNA")

  • min_cells (int, default: 0 ) –

    min cells a feature must be detected in to be kept

  • min_features (int, default: 0 ) –

    min features a cell must have to be kept

  • project (str, default: 'SeuratProject' ) –

    project name

  • feature_names (Optional[list[str]], default: None ) –

    optional list of feature (gene) names

  • cell_names (Optional[list[str]], default: None ) –

    optional list of cell barcodes

  • meta_data (Optional[DataFrame], default: None ) –

    optional per-cell metadata DataFrame

  • use_v5 (bool, default: True ) –

    if True, create Assay5 (v5); else Assay (v3)

Source code in truecell/truecell.py
def create_truecell_object(
    counts,
    assay: str = "RNA",
    min_cells: int = 0,
    min_features: int = 0,
    project: str = "SeuratProject",
    feature_names: Optional[list[str]] = None,
    cell_names: Optional[list[str]] = None,
    meta_data: Optional[pd.DataFrame] = None,
    use_v5: bool = True,
) -> Truecell:
    """Create a Truecell object from a counts matrix.

    Mirrors R's CreateSeuratObject().

    Parameters
    ----------
    counts       : sparse or dense matrix (features × cells)
    assay        : assay name (default "RNA")
    min_cells    : min cells a feature must be detected in to be kept
    min_features : min features a cell must have to be kept
    project      : project name
    feature_names: optional list of feature (gene) names
    cell_names   : optional list of cell barcodes
    meta_data    : optional per-cell metadata DataFrame
    use_v5       : if True, create Assay5 (v5); else Assay (v3)
    """
    key = f"{assay.lower()}_"

    # Each branch keeps its own narrowly-typed name and widens once, at the
    # end: the two classes hold their cells in differently-named slots, so a
    # single `assay_obj` reused across both branches has no type under which
    # both reads are valid.
    assay_obj: AnyAssay
    if use_v5:
        v5 = create_assay5_object(
            counts=counts,
            min_cells=min_cells,
            min_features=min_features,
            feature_names=feature_names,
            cell_names=cell_names,
            key=key,
        )
        cells = v5._all_cell_names
        assay_obj = v5
    else:
        v3 = create_assay_object(
            counts=counts,
            min_cells=min_cells,
            min_features=min_features,
            feature_names=feature_names,
            cell_names=cell_names,
            key=key,
        )
        cells = v3._cell_names
        assay_obj = v3

    # Build metadata. Both assay classes define `calc_n`, so the
    # `hasattr(assay_obj, "calc_n")` fallback that used to stand here was
    # unreachable — a second implementation of the same naming that nothing
    # exercised, deriving the suffix from the assay's *key* rather than from
    # the `assay` argument, and hardcoding `nCount_RNA` when the assay had no
    # default layer. The two agree for an ordinary RNA object, which is why
    # neither the tests nor the type checker had reason to look at it.
    raw_meta = assay_obj.calc_n()
    base_meta = raw_meta.rename(columns={"nCount": f"nCount_{assay}", "nFeature": f"nFeature_{assay}"})
    # `orig.ident` is the first column of every Seurat object's metadata and the
    # default identity class; scripts group and split on it. Seeded with the
    # project name, as `CreateSeuratObject` does.
    base_meta.insert(0, "orig.ident",
                     pd.Categorical([project] * len(cells), categories=[project]))

    if meta_data is not None:
        # Align user-supplied metadata to filtered cells
        supplied = meta_data.reindex(cells)
        for col in supplied.columns:
            base_meta[col] = supplied[col].values

    active_ident = pd.Categorical([project] * len(cells), categories=[project])

    obj = Truecell(
        assays={assay: assay_obj},
        meta_data=base_meta,
        active_assay=assay,
        active_ident=active_ident,
        project_name=project,
    )
    return obj

Assays

Assay5

Assay5(layers: dict[str, Union[ndarray, spmatrix]], feature_names: list[str], cell_names: list[str], assay_orig: Optional[str] = None, meta_data: Optional[DataFrame] = None, misc: Optional[dict] = None, key: str = 'rna_', default: int = 0, layer_features: Optional[dict[str, list[str]]] = None, layer_cells: Optional[dict[str, list[str]]] = None)

Bases: StdAssay

Modern layered assay (v5).

Mirrors R's Assay5 class from assay5.R. Extends StdAssay with no additional slots.

Source code in truecell/assay5.py
def __init__(
    self,
    layers: dict[str, Union[np.ndarray, sp.spmatrix]],
    feature_names: list[str],
    cell_names: list[str],
    assay_orig: Optional[str] = None,
    meta_data: Optional[pd.DataFrame] = None,
    misc: Optional[dict] = None,
    key: str = "rna_",
    default: int = 0,
    layer_features: Optional[dict[str, list[str]]] = None,
    layer_cells: Optional[dict[str, list[str]]] = None,
) -> None:
    self._key = key
    self.assay_orig = assay_orig
    self.misc = misc or {}
    self.default = default

    validate_feature_names(feature_names)
    validate_cell_names(cell_names)
    self._all_feature_names = list(feature_names)
    self._all_cell_names = list(cell_names)

    self.layers: dict[str, Union[np.ndarray, sp.spmatrix]] = {}
    self._cells = LogMap()
    self._features = LogMap()
    # Per-layer *ordered* feature / cell names. A layer may legitimately
    # span only a subset of the assay's features (e.g. scale.data holds
    # only the variable features, as in Seurat) or cells.
    self._layer_features: dict[str, list[str]] = {}
    self._layer_cells: dict[str, list[str]] = {}
    self._scaled_features: list[str] = []
    self._var_features: list[str] = []
    # Layer name -> the layer it was split from. See `_stem_of`.
    self._split_stems: dict[str, str] = {}

    layer_features = layer_features or {}
    layer_cells = layer_cells or {}
    for name, mat in layers.items():
        self._add_layer(
            name, mat,
            feature_names=layer_features.get(name),
            cell_names=layer_cells.get(name),
        )

    self.meta_data = (
        meta_data
        if meta_data is not None
        else pd.DataFrame(index=self._all_feature_names)
    )

create_assay5_object

create_assay5_object(counts=None, data=None, min_cells: int = 0, min_features: int = 0, feature_names: Optional[list[str]] = None, cell_names: Optional[list[str]] = None, key: str = 'rna_') -> Assay5
Source code in truecell/assay5.py
def create_assay5_object(
    counts=None,
    data=None,
    min_cells: int = 0,
    min_features: int = 0,
    feature_names: Optional[list[str]] = None,
    cell_names: Optional[list[str]] = None,
    key: str = "rna_",
) -> Assay5:
    matrix = counts if counts is not None else data
    if matrix is None:
        raise ValueError("Provide at least one of 'counts' or 'data'.")

    if is_lazy(matrix):
        # Keep the on-disk layer on disk. `np.asarray` below would read the
        # whole store into a dense array here in the constructor, so the
        # obvious way to use the feature -- open a store, build an object
        # around it -- would end the laziness before any analysis began.
        mat = matrix
    elif sp.issparse(matrix):
        mat = matrix.tocsc()
    else:
        mat = sp.csc_matrix(np.asarray(matrix))

    n_features, n_cells = mat.shape

    if feature_names is None:
        feature_names = [f"feature_{i}" for i in range(n_features)]
    if cell_names is None:
        cell_names = [f"cell_{i}" for i in range(n_cells)]

    # Filter features by min_cells. Either filter subsets the lazy store
    # through its own indexer, which yields an in-memory sparse block of just
    # the kept entries -- a filtered assay cannot stay on disk without writing
    # a second store, but it need never be dense to get there.
    if min_cells > 0:
        nnz_per_feat = (mat.nnz_per_row() if is_lazy(mat)
                        else np.diff(mat.T.tocsc().indptr))
        keep_feat = np.where(nnz_per_feat >= min_cells)[0]
        mat = mat[keep_feat, :]
        feature_names = [feature_names[i] for i in keep_feat]

    # Filter cells by min_features
    if min_features > 0:
        nnz_per_cell = (mat.nnz_per_col() if is_lazy(mat)
                        else np.diff(mat.tocsc().indptr))
        keep_cell = np.where(nnz_per_cell >= min_features)[0]
        mat = mat[:, keep_cell]
        cell_names = [cell_names[i] for i in keep_cell]

    layer_name = "counts" if counts is not None else "data"
    return Assay5(
        layers={layer_name: mat},
        feature_names=list(feature_names),
        cell_names=list(cell_names),
        key=key,
    )

StdAssay

StdAssay(layers: dict[str, Union[ndarray, spmatrix]], feature_names: list[str], cell_names: list[str], assay_orig: Optional[str] = None, meta_data: Optional[DataFrame] = None, misc: Optional[dict] = None, key: str = 'rna_', default: int = 0, layer_features: Optional[dict[str, list[str]]] = None, layer_cells: Optional[dict[str, list[str]]] = None)

Bases: KeyMixin, ABC

Abstract base for layered assays (v5 architecture).

Mirrors R's StdAssay virtual class from assay5.R. Unlike the legacy Assay (v3), StdAssay stores arbitrary named layers and uses LogMap to track which cells/features belong to each layer.

Slots
  • layers (dict[str, AnyMatrix]) — named expression matrices (features × cells)
  • cells (LogMap) — per-layer boolean cell membership
  • features (LogMap) — per-layer boolean feature membership
  • default (int) — index of the default layer
  • assay_orig (Optional[str])
  • meta_data (pd.DataFrame) — per-feature metadata
  • misc (dict)
  • _key (str) — inherited from KeyMixin
Source code in truecell/assay5.py
def __init__(
    self,
    layers: dict[str, Union[np.ndarray, sp.spmatrix]],
    feature_names: list[str],
    cell_names: list[str],
    assay_orig: Optional[str] = None,
    meta_data: Optional[pd.DataFrame] = None,
    misc: Optional[dict] = None,
    key: str = "rna_",
    default: int = 0,
    layer_features: Optional[dict[str, list[str]]] = None,
    layer_cells: Optional[dict[str, list[str]]] = None,
) -> None:
    self._key = key
    self.assay_orig = assay_orig
    self.misc = misc or {}
    self.default = default

    validate_feature_names(feature_names)
    validate_cell_names(cell_names)
    self._all_feature_names = list(feature_names)
    self._all_cell_names = list(cell_names)

    self.layers: dict[str, Union[np.ndarray, sp.spmatrix]] = {}
    self._cells = LogMap()
    self._features = LogMap()
    # Per-layer *ordered* feature / cell names. A layer may legitimately
    # span only a subset of the assay's features (e.g. scale.data holds
    # only the variable features, as in Seurat) or cells.
    self._layer_features: dict[str, list[str]] = {}
    self._layer_cells: dict[str, list[str]] = {}
    self._scaled_features: list[str] = []
    self._var_features: list[str] = []
    # Layer name -> the layer it was split from. See `_stem_of`.
    self._split_stems: dict[str, str] = {}

    layer_features = layer_features or {}
    layer_cells = layer_cells or {}
    for name, mat in layers.items():
        self._add_layer(
            name, mat,
            feature_names=layer_features.get(name),
            cell_names=layer_cells.get(name),
        )

    self.meta_data = (
        meta_data
        if meta_data is not None
        else pd.DataFrame(index=self._all_feature_names)
    )

set_layer_data

set_layer_data(layer: str, value: Union[ndarray, spmatrix], cell_names: Optional[list[str]] = None, feature_names: Optional[list[str]] = None) -> None

Store (or replace) a layer.

feature_names / cell_names declare which features / cells the matrix spans, so a layer may legitimately cover only a subset (e.g. scale.data over the variable features). When replacing an existing layer without supplying names, the previous span is reused.

Source code in truecell/assay5.py
def set_layer_data(
    self,
    layer: str,
    value: Union[np.ndarray, sp.spmatrix],
    cell_names: Optional[list[str]] = None,
    feature_names: Optional[list[str]] = None,
) -> None:
    """Store (or replace) a layer.

    ``feature_names`` / ``cell_names`` declare which features / cells the
    matrix spans, so a layer may legitimately cover only a subset (e.g.
    scale.data over the variable features). When replacing an existing
    layer without supplying names, the previous span is reused.
    """
    if layer in self.layers:
        if feature_names is None:
            feature_names = self._layer_features.get(layer)
        if cell_names is None:
            cell_names = self._layer_cells.get(layer)
        del self.layers[layer]
    self._add_layer(layer, value, feature_names=feature_names, cell_names=cell_names)

join_layers

join_layers(layers: Optional[list[str]] = None) -> Self

Rejoin split layers, restoring the name, order and contents.

Mirrors R's JoinLayers. Each split stem is rejoined separately — counts.batch1 and counts.batch2 become counts again — and layers that were never split are left alone, which is what makes the no-argument call safe on a prepared assay that also holds data and a variable-features-only scale.data.

The rejoined columns come back in the assay's cell order, not in the order the split happened to produce. The assay's own cell vector never moved during the split, so anything else would leave the matrix silently transposed against the metadata that indexes it.

Source code in truecell/assay5.py
def join_layers(self, layers: Optional[list[str]] = None) -> Self:
    """Rejoin split layers, restoring the name, order and contents.

    Mirrors R's ``JoinLayers``. Each split *stem* is rejoined separately —
    ``counts.batch1`` and ``counts.batch2`` become ``counts`` again — and
    layers that were never split are left alone, which is what makes the
    no-argument call safe on a prepared assay that also holds ``data`` and a
    variable-features-only ``scale.data``.

    The rejoined columns come back in the **assay's** cell order, not in the
    order the split happened to produce. The assay's own cell vector never
    moved during the split, so anything else would leave the matrix silently
    transposed against the metadata that indexes it.
    """
    if layers is not None:
        # Explicit list: caller names the parts, so take the stem from the
        # recorded provenance and fall back to the shared prefix.
        parts = [n for n in layers if n in self.layers]
        if not parts:
            return self
        stems = {self._stem_of(n) for n in parts}
        stem = stems.pop() if len(stems) == 1 and None not in stems else None
        if stem is None:
            stem = os.path.commonprefix(parts).rstrip(_SPLIT_SEP)
        groups = {stem or parts[0]: parts}
    else:
        groups = {}
        for name in self.layers:
            stem = self._stem_of(name)
            if stem is not None:
                groups.setdefault(stem, []).append(name)
    if not groups:
        return self

    new_obj = self._copy()
    for stem, parts in groups.items():
        features = list(self._layer_features.get(
            parts[0], self._all_feature_names))
        for part in parts[1:]:
            if list(self._layer_features.get(part, self._all_feature_names)) != features:
                raise ValueError(
                    f"Cannot join layers {parts!r}: they do not span the "
                    f"same features."
                )

        mats = [self.layers[p] for p in parts]
        combined = (sp.hstack(mats, format="csc") if sp.issparse(mats[0])
                    else np.hstack(mats))

        # Column j of `combined` belongs to concat_cells[j]; put them back
        # into the assay's order.
        concat_cells = [c for p in parts
                        for c in self._layer_cells.get(p, self._all_cell_names)]
        position = {c: j for j, c in enumerate(concat_cells)}
        ordered = [c for c in self._all_cell_names if c in position]
        combined = combined[:, [position[c] for c in ordered]]

        for part in parts:
            new_obj._drop_layer(part)
        new_obj._add_layer(stem, combined,
                           feature_names=features, cell_names=ordered)
    return new_obj

split_layers

split_layers(f: list[str], layer: Optional[str] = None) -> Self

Split one layer into per-group layers, as R's split() does.

The parts are named <layer>.<group> — Seurat's spelling, which users match on with Layers(obj, pattern = "counts") — and each records the layer it came from so join_layers can put it back.

Source code in truecell/assay5.py
def split_layers(self, f: list[str], layer: Optional[str] = None) -> Self:
    """Split one layer into per-group layers, as R's ``split()`` does.

    The parts are named ``<layer>.<group>`` — Seurat's spelling, which users
    match on with ``Layers(obj, pattern = "counts")`` — and each records the
    layer it came from so :meth:`join_layers` can put it back.
    """
    if layer is None:
        layer = self.default_layer
    if layer is None:
        raise ValueError("No layer to split.")
    mat = self.layers[layer]
    if len(f) != mat.shape[1]:
        raise ValueError("f must have one entry per cell.")

    groups: dict[str, list[int]] = {}
    for i, g in enumerate(f):
        groups.setdefault(str(g), []).append(i)

    new_obj = self._copy()
    base_feats = list(new_obj._layer_features.get(layer, new_obj._all_feature_names))
    base_cells = list(new_obj._layer_cells.get(layer, new_obj._all_cell_names))
    new_obj._drop_layer(layer)
    for g, idxs in groups.items():
        key = f"{layer}{_SPLIT_SEP}{g}"
        new_obj._add_layer(
            key, mat[:, idxs],
            feature_names=base_feats,
            cell_names=[base_cells[i] for i in idxs],
        )
        new_obj._split_stems[key] = layer
    return new_obj

Assay

Assay(counts: Optional[Union[ndarray, spmatrix]] = None, data: Optional[Union[ndarray, spmatrix]] = None, scale_data: Optional[ndarray] = None, scaled_features: Optional[list[str]] = None, feature_names: Optional[list[str]] = None, cell_names: Optional[list[str]] = None, assay_orig: Optional[str] = None, var_features: Optional[list[str]] = None, meta_features: Optional[DataFrame] = None, misc: Optional[dict] = None, key: str = 'rna_')

Bases: KeyMixin

Legacy (v3) Assay object.

Mirrors R's Assay class from assay.R.

Slots
  • counts — raw counts / TPMs (features × cells)
  • data — normalised expression (features × cells)
  • scale_data — scaled expression (features × cells, dense) — a subset of the features, since ScaleData defaults to the variable ones. R's slot is a matrix and carries its own rownames; a bare ndarray does not, so the labels live alongside it in _scaled_features and every read of the layer goes through features("scale_data").
  • assay_orig — name of original assay this was derived from
  • var_features — list of highly variable feature names
  • meta_features — per-feature metadata DataFrame (features × cols)
  • misc (dict) — for miscellaneous storage
  • _key (str) — ing key prefix (inherited from KeyMixin)
Source code in truecell/assay.py
def __init__(
    self,
    counts: Optional[Union[np.ndarray, sp.spmatrix]] = None,
    data: Optional[Union[np.ndarray, sp.spmatrix]] = None,
    scale_data: Optional[np.ndarray] = None,
    scaled_features: Optional[list[str]] = None,
    feature_names: Optional[list[str]] = None,
    cell_names: Optional[list[str]] = None,
    assay_orig: Optional[str] = None,
    var_features: Optional[list[str]] = None,
    meta_features: Optional[pd.DataFrame] = None,
    misc: Optional[dict] = None,
    key: str = "rna_",
) -> None:
    self.key = key  # validated by KeyMixin setter

    # Resolve which matrix to use
    if counts is not None and data is None:
        matrix = counts
    elif data is not None and counts is None:
        matrix = data
    elif counts is not None and data is not None:
        matrix = counts  # counts takes precedence for shape
    else:
        raise ValueError("Provide at least one of 'counts' or 'data'.")

    n_features = matrix.shape[0]
    n_cells = matrix.shape[1]

    if feature_names is None:
        feature_names = [f"feature_{i}" for i in range(n_features)]
    if cell_names is None:
        cell_names = [f"cell_{i}" for i in range(n_cells)]

    validate_feature_names(feature_names)
    validate_cell_names(cell_names)

    if len(feature_names) != n_features:
        raise ValueError(
            f"feature_names length {len(feature_names)} != matrix rows {n_features}."
        )
    if len(cell_names) != n_cells:
        raise ValueError(
            f"cell_names length {len(cell_names)} != matrix cols {n_cells}."
        )

    self._feature_names = list(feature_names)
    self._cell_names = list(cell_names)

    self.counts = counts if counts is not None else empty_sparse(n_features, n_cells)
    self.data = data if data is not None else self.counts
    self.scale_data = scale_data if scale_data is not None else empty_dense(0, n_cells)
    self._scaled_features = self._resolve_scaled_features(scaled_features)
    self.assay_orig = assay_orig
    self.var_features = list(var_features) if var_features else []
    self.meta_features = (
        meta_features
        if meta_features is not None
        else pd.DataFrame(index=self._feature_names)
    )
    self.misc = misc or {}

create_assay_object

create_assay_object(counts=None, data=None, min_cells: int = 0, min_features: int = 0, feature_names: Optional[list[str]] = None, cell_names: Optional[list[str]] = None, key: str = 'rna_') -> Assay

Create an Assay, optionally filtering by min_cells / min_features.

Source code in truecell/assay.py
def create_assay_object(
    counts=None,
    data=None,
    min_cells: int = 0,
    min_features: int = 0,
    feature_names: Optional[list[str]] = None,
    cell_names: Optional[list[str]] = None,
    key: str = "rna_",
) -> Assay:
    """Create an Assay, optionally filtering by min_cells / min_features."""
    matrix = counts if counts is not None else data

    if matrix is None:
        raise ValueError("Provide at least one of 'counts' or 'data'.")

    if sp.issparse(matrix):
        mat_csc = matrix.tocsc()
    else:
        mat_csc = sp.csc_matrix(np.asarray(matrix))

    n_features, n_cells = mat_csc.shape

    if feature_names is None:
        feature_names = [f"feature_{i}" for i in range(n_features)]
    if cell_names is None:
        cell_names = [f"cell_{i}" for i in range(n_cells)]

    # Filter features by min_cells
    if min_cells > 0:
        cell_counts_per_feature = np.diff(mat_csc.T.tocsc().indptr)
        keep_feat = np.where(cell_counts_per_feature >= min_cells)[0]
        mat_csc = mat_csc[keep_feat, :]
        feature_names = [feature_names[i] for i in keep_feat]

    # Filter cells by min_features
    if min_features > 0:
        feat_counts_per_cell = np.diff(mat_csc.tocsc().indptr)
        keep_cell = np.where(feat_counts_per_cell >= min_features)[0]
        mat_csc = mat_csc[:, keep_cell]
        cell_names = [cell_names[i] for i in keep_cell]

    if counts is not None:
        return Assay(counts=mat_csc, feature_names=feature_names, cell_names=cell_names, key=key)
    return Assay(data=mat_csc, feature_names=feature_names, cell_names=cell_names, key=key)

Reductions, graphs and neighbours

DimReduc

DimReduc(cell_embeddings: ndarray, cell_names: list[str], feature_loadings: Optional[ndarray] = None, feature_names: Optional[list[str]] = None, feature_loadings_projected: Optional[ndarray] = None, assay_used: str = '', global_: bool = False, stdev: Optional[ndarray] = None, jackstraw: Optional[JackStrawData] = None, misc: Optional[dict] = None, key: str = 'PC_')

Bases: KeyMixin

Stores a dimensionality reduction (PCA, UMAP, tSNE, …).

Mirrors R's DimReduc class from dimreduc.R.

Slots
  • cell_embeddings (np.ndarray) — (n_cells × n_dims), required
  • feature_loadings (np.ndarray) — (n_features × n_dims), optional
  • feature_loadings_projected (np.ndarray) — projected loadings, optional
  • assay_used (str) — source assay name
  • global_ (bool) — if True, persists when assay is removed
  • stdev (np.ndarray) — per-dimension std devs
  • jackstraw (JackStrawData)
  • misc (dict)
  • _key (str) — prefix, e.g. "PC_"
Source code in truecell/dimreduc.py
def __init__(
    self,
    cell_embeddings: np.ndarray,
    cell_names: list[str],
    feature_loadings: Optional[np.ndarray] = None,
    feature_names: Optional[list[str]] = None,
    feature_loadings_projected: Optional[np.ndarray] = None,
    assay_used: str = "",
    global_: bool = False,
    stdev: Optional[np.ndarray] = None,
    jackstraw: Optional[JackStrawData] = None,
    misc: Optional[dict] = None,
    key: str = "PC_",
) -> None:
    self._key = key
    self.cell_embeddings = np.asarray(cell_embeddings)
    self._cell_names = list(cell_names)

    if len(self._cell_names) != self.cell_embeddings.shape[0]:
        raise ValueError(
            f"cell_names length {len(self._cell_names)} != "
            f"cell_embeddings rows {self.cell_embeddings.shape[0]}."
        )

    n_dims = self.cell_embeddings.shape[1]

    if feature_loadings is not None:
        self.feature_loadings = np.asarray(feature_loadings)
    else:
        self.feature_loadings = empty_dense(0, n_dims)

    self._feature_names = list(feature_names) if feature_names else []

    if feature_loadings_projected is not None:
        self.feature_loadings_projected = np.asarray(feature_loadings_projected)
    else:
        self.feature_loadings_projected = empty_dense(0, n_dims)

    self.assay_used = assay_used
    self.global_ = global_
    self.stdev = np.asarray(stdev) if stdev is not None else np.array([])
    self.jackstraw = jackstraw if jackstraw is not None else JackStrawData()
    self.misc = misc or {}

Graph

Graph(matrix: spmatrix, cell_names: list[str], assay_used: Optional[str] = None)

Sparse graph object for cell-cell relationships (e.g. SNN graph).

Mirrors R's Graph class from graph.R, which extends dgCMatrix. In Python we wrap (not inherit) a scipy CSC matrix to avoid scipy subclassing pitfalls.

Slots
  • _matrix (scipy.sparse.csc_matrix) — underlying adjacency matrix
  • assay_used (Optional[str]) — assay that generated this graph
  • _cell_names (list[str]) — row/col names (cells)
Source code in truecell/graph.py
def __init__(
    self,
    matrix: sp.spmatrix,
    cell_names: list[str],
    assay_used: Optional[str] = None,
) -> None:
    if not sp.issparse(matrix):
        matrix = sp.csc_matrix(matrix)
    else:
        matrix = matrix.tocsc()

    n = len(cell_names)
    if matrix.shape != (n, n):
        raise ValueError(
            f"matrix shape {matrix.shape} does not match "
            f"cell_names length {n} × {n}."
        )
    if len(cell_names) != len(set(cell_names)):
        raise ValueError("cell_names must be unique.")

    self._matrix = matrix
    self._cell_names = list(cell_names)
    self.assay_used = assay_used
    self._validate()

subset

subset(cells: list[str]) -> 'Graph'

Return a new Graph restricted to cells (cell×cell submatrix).

Source code in truecell/graph.py
def subset(self, cells: list[str]) -> "Graph":
    """Return a new Graph restricted to ``cells`` (cell×cell submatrix)."""
    idx_map = {c: i for i, c in enumerate(self._cell_names)}
    keep = [c for c in cells if c in idx_map]
    idx = [idx_map[c] for c in keep]
    sub = self._matrix[np.ix_(idx, idx)]
    return Graph(matrix=sub, cell_names=keep, assay_used=self.assay_used)

as_graph

as_graph(x: Union[ndarray, spmatrix, 'Neighbor'], cell_names: Optional[list[str]] = None, assay_used: Optional[str] = None, weighted: bool = True) -> Graph

Convert a matrix or Neighbor to a Graph. Mirrors R as.Graph().

Source code in truecell/graph.py
def as_graph(
    x: Union[np.ndarray, sp.spmatrix, "Neighbor"],
    cell_names: Optional[list[str]] = None,
    assay_used: Optional[str] = None,
    weighted: bool = True,
) -> Graph:
    """Convert a matrix or Neighbor to a Graph.  Mirrors R as.Graph()."""
    from .neighbor import Neighbor

    if isinstance(x, Graph):
        return x

    if isinstance(x, Neighbor):
        g = x.as_graph(weighted=weighted)
        if assay_used is not None:
            g.assay_used = assay_used
        return g

    if sp.issparse(x) or isinstance(x, np.ndarray):
        if cell_names is None:
            n = x.shape[0]
            cell_names = [str(i) for i in range(n)]
        return Graph(matrix=x, cell_names=cell_names, assay_used=assay_used)

    raise TypeError(f"Cannot convert {type(x).__name__} to Graph.")

Neighbor

Neighbor(nn_idx: ndarray, nn_dist: ndarray, cell_names: list[str], alg_idx: Any = None, alg_info: Optional[dict] = None)

Nearest-neighbor results for a set of cells.

Mirrors R's Neighbor class from neighbor.R.

Slots
  • nn_idx (int matrix) — (n_cells × k), neighbor indices (1-based in R; 0-based here)
  • nn_dist (float matrix) — (n_cells × k), corresponding distances
  • alg_idx (Any) — algorithm index object (e.g. annoy index)
  • alg_info (dict) — metadata about the algorithm used
  • cell_names (list[str]) — cell barcodes, length n_cells
Source code in truecell/neighbor.py
def __init__(
    self,
    nn_idx: np.ndarray,
    nn_dist: np.ndarray,
    cell_names: list[str],
    alg_idx: Any = None,
    alg_info: Optional[dict] = None,
) -> None:
    self.nn_idx = np.asarray(nn_idx, dtype=int)
    self.nn_dist = np.asarray(nn_dist, dtype=float)
    self.cell_names = list(cell_names)
    self.alg_idx = alg_idx
    self.alg_info = alg_info or {}
    self._validate()

Supporting structures

JackStrawData

JackStrawData(empirical_p_values: Optional[ndarray] = None, fake_reduction_scores: Optional[ndarray] = None, overall_p_values: Optional[ndarray] = None, score: Optional[ndarray] = None, method: Optional[str] = None)

Stores JackStraw permutation test results for a DimReduc.

Mirrors R's JackStraw / JackStrawData from jackstraw.R.

Source code in truecell/jackstraw.py
def __init__(
    self,
    empirical_p_values: Optional[np.ndarray] = None,
    fake_reduction_scores: Optional[np.ndarray] = None,
    overall_p_values: Optional[np.ndarray] = None,
    score: Optional[np.ndarray] = None,
    method: Optional[str] = None,
) -> None:
    self.empirical_p_values = empirical_p_values
    self.fake_reduction_scores = fake_reduction_scores
    self.overall_p_values = overall_p_values
    self.score = score
    self.method = method

LogMap

LogMap(data: dict[str, ndarray] | None = None)

Logical mapping: named boolean arrays indicating membership.

Mirrors R's LogMap class from logmap.R. Keys are cell or feature names; values are boolean numpy arrays.

Source code in truecell/logmap.py
def __init__(self, data: dict[str, np.ndarray] | None = None) -> None:
    self._map: dict[str, np.ndarray] = {}
    if data:
        for k, v in data.items():
            self[k] = v

KeyMixin

Mixin providing a validated 'key' slot, mirroring R's KeyMixin from keymixin.R.

TruecellCommand

TruecellCommand(name: str, time_stamp: Optional[datetime] = None, assay_used: Optional[str] = None, call_string: str = '', params: Optional[dict] = None, key: Optional[str] = None)

Logs commands executed on a Truecell object.

Mirrors R's TruecellCommand class from command.R.

Slots
  • name (str) — function/method name
  • time_stamp (datetime) — when the command ran
  • assay_used (Optional[str])
  • call_string (str) — human-readable call representation
  • params (dict) — non-function parameters passed to the command
Source code in truecell/command.py
def __init__(
    self,
    name: str,
    time_stamp: Optional[datetime] = None,
    assay_used: Optional[str] = None,
    call_string: str = "",
    params: Optional[dict] = None,
    key: Optional[str] = None,
) -> None:
    self.name = name
    self.time_stamp = time_stamp or datetime.now()
    self.assay_used = assay_used
    self.call_string = call_string
    self.params = params or {}
    #: How R indexes this entry in ``obj@commands`` — the command name, the
    #: assay, and for reduction-consuming steps the reduction, joined with
    #: dots: ``FindNeighbors.RNA.pca``. Scripts look entries up by this, so
    #: it is data rather than decoration and matches Seurat's spelling.
    self.key = key or name

log_truecell_command

log_truecell_command(object_, func_name: str, params: Optional[dict] = None, assay: Optional[str] = None, reduction: Optional[str] = None) -> 'TruecellCommand'

Capture a command log entry, typically called at the end of a function.

Mirrors R's LogSeuratCommand, including how it names the entry: Seurat's function name, the assay, and the reduction where one was consumed. The names are R's (RunPCA, not run_pca) because the log is a lookup table users query — the same reasoning that keeps layer names scale.data and reduction keys PC_.

Source code in truecell/command.py
def log_truecell_command(
    object_,
    func_name: str,
    params: Optional[dict] = None,
    assay: Optional[str] = None,
    reduction: Optional[str] = None,
) -> "TruecellCommand":
    """Capture a command log entry, typically called at the end of a function.

    Mirrors R's LogSeuratCommand, including how it names the entry: Seurat's
    function name, the assay, and the reduction where one was consumed. The
    names are R's (``RunPCA``, not ``run_pca``) because the log is a lookup
    table users query — the same reasoning that keeps layer names ``scale.data``
    and reduction keys ``PC_``.
    """
    params = params or {}
    # Remove any callable values (functions/lambdas) from params log
    safe_params = {k: v for k, v in params.items() if not callable(v)}

    call_parts = [f"{func_name}("]
    parts = [f"{k}={v!r}" for k, v in safe_params.items()]
    call_parts.append(", ".join(parts))
    call_parts.append(")")
    call_string = "".join(call_parts)

    key = ".".join(p for p in (func_name, assay, reduction) if p)
    cmd = TruecellCommand(
        name=func_name,
        assay_used=assay,
        call_string=call_string,
        params=safe_params,
        key=key,
    )
    # Append to object's command log if it has one
    if hasattr(object_, "commands") and isinstance(object_.commands, list):
        object_.commands.append(cmd)
    return cmd