Skip to content

Integration and reference mapping

Two different jobs that share machinery. Integration removes a batch effect between datasets you intend to analyse together. Reference mapping leaves the reference untouched and projects a query into it, carrying labels across.

integrate_layers is the v5 dispatcher — method="harmony" | "cca" | "rpca" — and runs integrate_embeddings, the embedding-space algorithm, as Seurat v5 does. The v4 pair (find_integration_anchors + integrate_data, which corrects expression rather than embeddings) is still available directly and is still what you want if you are reproducing a v4 analysis. These were the same function once, which was a bug: the v5 name ran the v4 algorithm.

Both anchor paths are compared against Seurat's own anchors, not just against the clustering they produce, in Anchor internals.

Batch correction

integrate_layers

integrate_layers(seurat, method: str = 'harmony', orig_reduction: str = 'pca', new_reduction: Optional[str] = None, group_by: Optional[Union[str, list[str]]] = None, assay: Optional[str] = None, **kwargs) -> None

Integrate layers/batches (Seurat v5 IntegrateLayers dispatch API).

Mirrors IntegrateLayers(obj, method = HarmonyIntegration, orig.reduction = "pca"). A thin dispatcher over the individual integration routines.

Parameters:

  • method (str, default: 'harmony' ) –

    'harmony', 'cca', or 'rpca'.

  • orig_reduction (str, default: 'pca' ) –

    reduction to integrate (default 'pca'). Every method corrects this reduction and writes a new one of the same shape — the anchor methods included, which is what makes them interchangeable with Harmony here.

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

    storage key for the integrated reduction (defaults to '{method}')

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

    batch column identifying the layers/batches to integrate (required for every method)

Source code in truecell/integration.py
def integrate_layers(
    seurat,
    method: str = "harmony",
    orig_reduction: str = "pca",
    new_reduction: Optional[str] = None,
    group_by: Optional[Union[str, list[str]]] = None,
    assay: Optional[str] = None,
    **kwargs,
) -> None:
    """Integrate layers/batches (Seurat v5 ``IntegrateLayers`` dispatch API).

    Mirrors ``IntegrateLayers(obj, method = HarmonyIntegration,
    orig.reduction = "pca")``. A thin dispatcher over the individual
    integration routines.

    Parameters
    ----------
    method         : 'harmony', 'cca', or 'rpca'.
    orig_reduction : reduction to integrate (default 'pca'). Every method
                     corrects this reduction and writes a new one of the same
                     shape — the anchor methods included, which is what makes
                     them interchangeable with Harmony here.
    new_reduction  : storage key for the integrated reduction
                     (defaults to '{method}')
    group_by       : batch column identifying the layers/batches to integrate
                     (required for every method)
    """
    method = method.lower()
    new_reduction = new_reduction or method

    if method in ("harmony", "harmonyintegration"):
        if group_by is None:
            raise ValueError("method='harmony' requires group_by (batch column).")
        run_harmony(
            seurat,
            group_by=group_by,
            reduction=orig_reduction,
            reduction_name=new_reduction,
            assay=assay,
            **kwargs,
        )
    elif method in ("cca", "rpca", "ccaintegration", "rpcaintegration"):
        if group_by is None:
            raise ValueError(
                f"method={method!r} requires group_by (batch column)."
            )
        reduction = "rpca" if method.startswith("rpca") else "cca"
        _integrate_anchor_reduction(
            seurat,
            group_by=group_by,
            reduction=reduction,
            new_reduction=new_reduction,
            orig_reduction=orig_reduction,
            assay=assay,
            **kwargs,
        )
    else:
        raise ValueError(
            f"Unknown integration method {method!r}. "
            "Supported: 'harmony', 'cca', 'rpca'."
        )

run_harmony

run_harmony(seurat, group_by: Union[str, list[str]], reduction: str = 'pca', dims: Optional[Union[list[int], range]] = None, reduction_name: str = 'harmony', reduction_key: str = 'harmony_', theta: Optional[Union[float, list[float]]] = None, lambda_: Optional[Union[float, list[float]]] = None, sigma: float = 0.1, nclust: Optional[int] = None, max_iter_harmony: int = 10, assay: Optional[str] = None, seed: int = 0) -> None

Run Harmony batch correction on an existing reduction.

Mirrors R's RunHarmony(obj, group.by.vars = "batch"). Takes the cell embeddings of reduction (PCA by default), removes batch effects with harmonypy, and stores the corrected embeddings as a new DimReduc under reduction_name — same shape as the input, so it can be passed straight to find_neighbors(reduction="harmony") / run_umap(reduction="harmony").

Parameters:

  • group_by (Union[str, list[str]]) –

    metadata column(s) identifying the batch(es) to correct

  • reduction (str, default: 'pca' ) –

    source reduction to correct (default 'pca')

  • dims (Optional[Union[list[int], range]], default: None ) –

    which dimensions of reduction to use (0-indexed; default all available)

  • reduction_name (str, default: 'harmony' ) –

    storage key for the corrected reduction

  • theta (Optional[Union[float, list[float]]], default: None ) –

    diversity clustering penalty (harmonypy default when None)

  • lambda_ (Optional[Union[float, list[float]]], default: None ) –

    ridge regression penalty (harmonypy default when None)

  • sigma (float, default: 0.1 ) –

    soft-clustering width

  • nclust (Optional[int], default: None ) –

    number of Harmony clusters (harmonypy default when None)

  • max_iter_harmony (int, default: 10 ) –

    maximum Harmony iterations

  • seed (int, default: 0 ) –

    random seed for reproducibility

Source code in truecell/integration.py
def run_harmony(
    seurat,
    group_by: Union[str, list[str]],
    reduction: str = "pca",
    dims: Optional[Union[list[int], range]] = None,
    reduction_name: str = "harmony",
    reduction_key: str = "harmony_",
    theta: Optional[Union[float, list[float]]] = None,
    lambda_: Optional[Union[float, list[float]]] = None,
    sigma: float = 0.1,
    nclust: Optional[int] = None,
    max_iter_harmony: int = 10,
    assay: Optional[str] = None,
    seed: int = 0,
) -> None:
    """Run Harmony batch correction on an existing reduction.

    Mirrors R's ``RunHarmony(obj, group.by.vars = "batch")``. Takes the cell
    embeddings of ``reduction`` (PCA by default), removes batch effects with
    ``harmonypy``, and stores the corrected embeddings as a new DimReduc under
    ``reduction_name`` — same shape as the input, so it can be passed straight
    to ``find_neighbors(reduction="harmony")`` / ``run_umap(reduction="harmony")``.

    Parameters
    ----------
    group_by         : metadata column(s) identifying the batch(es) to correct
    reduction        : source reduction to correct (default 'pca')
    dims             : which dimensions of ``reduction`` to use (0-indexed;
                       default all available)
    reduction_name   : storage key for the corrected reduction
    theta            : diversity clustering penalty (harmonypy default when None)
    lambda_          : ridge regression penalty (harmonypy default when None)
    sigma            : soft-clustering width
    nclust           : number of Harmony clusters (harmonypy default when None)
    max_iter_harmony : maximum Harmony iterations
    seed             : random seed for reproducibility
    """
    try:
        import harmonypy
    except ImportError as exc:  # pragma: no cover - exercised only without the dep
        raise ImportError(
            "run_harmony requires 'harmonypy'. Install it with "
            "`pip install truecell[integration]` or `pip install harmonypy`."
        ) from exc

    assay_name = assay or seurat.active_assay

    if reduction not in seurat.reductions:
        raise KeyError(
            f"Reduction '{reduction}' not found. Run run_pca() first."
        )
    dr = seurat.reductions[reduction]
    embeddings = dr.cell_embeddings  # (cells × dims)

    if dims is not None:
        embeddings = embeddings[:, list(dims)]

    group_vars = [group_by] if isinstance(group_by, str) else list(group_by)
    missing = [g for g in group_vars if g not in seurat.meta_data.columns]
    if missing:
        raise KeyError(
            f"group_by column(s) {missing} not found in meta_data."
        )
    meta = seurat.meta_data[group_vars]

    n_cells = embeddings.shape[0]

    np.random.seed(seed)
    harmony_obj = harmonypy.run_harmony(
        embeddings,
        meta,
        group_vars,
        theta=theta,
        lamb=lambda_,
        sigma=sigma,
        nclust=nclust,
        max_iter_harmony=max_iter_harmony,
        random_state=seed,
    )
    # harmonypy stores corrected embeddings as (dims × cells); orient robustly
    # to (cells × dims) by matching the known cell count.
    corrected = np.asarray(harmony_obj.Z_corr)
    if corrected.shape[0] != n_cells and corrected.shape[1] == n_cells:
        corrected = corrected.T

    cells = seurat.cell_names()
    dim_names = [f"{reduction_key}{i + 1}" for i in range(corrected.shape[1])]

    seurat.reductions[reduction_name] = DimReduc(
        cell_embeddings=corrected,
        assay_used=assay_name,
        key=reduction_key,
        cell_names=cells,
        feature_names=dim_names,
    )

Anchors, directly

find_integration_anchors

find_integration_anchors(objects: list, anchor_features: int | list[str] | None = 2000, reduction: str = 'cca', dims: int = 30, k_anchor: int = 5, k_filter: int = 200, k_score: int = 30, reference: int = 0, layer: str = 'scale.data', seed: int = 42) -> IntegrationAnchors

Find anchors linking each dataset to the reference (Seurat's FindIntegrationAnchors).

Mirrors FindIntegrationAnchors(object.list, reduction = "cca"). Anchors are mutual nearest neighbours in a shared CCA (or reciprocal-PCA) space, scored by neighbourhood consistency and filtered against the original expression space.

Parameters:

  • objects (list) –

    list of Truecell objects (each normalized + with variable features / scaled data). objects[reference] is treated as the reference every other dataset is anchored to.

  • anchor_features (int | list[str] | None, default: 2000 ) –

    the features to integrate on, or how many to choose with select_integration_features. 2000 by default, as Seurat's anchor.features; None means the default.

  • reduction (str, default: 'cca' ) –

    "cca" or "rpca".

  • dims (int, default: 30 ) –

    number of shared dimensions to use.

  • k_anchor (int, default: 5 ) –

    neighbours for the mutual-nearest-neighbour search.

  • k_filter (int, default: 200 ) –

    neighbourhood size for the feature-space anchor filter (set to 0 or None to skip filtering).

  • k_score (int, default: 30 ) –

    neighbourhood size for anchor scoring.

  • reference (int, default: 0 ) –

    index of the reference dataset in objects.

  • layer (str, default: 'scale.data' ) –

    layer to draw the shared-space expression from.

  • seed (int, default: 42 ) –

    random seed for the PCA/neighbour steps.

Returns:

Source code in truecell/anchors.py
def find_integration_anchors(
    objects: list,
    anchor_features: int | list[str] | None = 2000,
    reduction: str = "cca",
    dims: int = 30,
    k_anchor: int = 5,
    k_filter: int = 200,
    k_score: int = 30,
    reference: int = 0,
    layer: str = "scale.data",
    seed: int = 42,
) -> IntegrationAnchors:
    """Find anchors linking each dataset to the reference (Seurat's ``FindIntegrationAnchors``).

    Mirrors ``FindIntegrationAnchors(object.list, reduction = "cca")``. Anchors
    are mutual nearest neighbours in a shared CCA (or reciprocal-PCA) space,
    scored by neighbourhood consistency and filtered against the original
    expression space.

    Parameters
    ----------
    objects         : list of Truecell objects (each normalized + with variable
                      features / scaled data). ``objects[reference]`` is treated
                      as the reference every other dataset is anchored to.
    anchor_features : the features to integrate on, or how many to choose with
                      :func:`select_integration_features`. 2000 by default, as
                      Seurat's ``anchor.features``; ``None`` means the default.
    reduction       : ``"cca"`` or ``"rpca"``.
    dims            : number of shared dimensions to use.
    k_anchor        : neighbours for the mutual-nearest-neighbour search.
    k_filter        : neighbourhood size for the feature-space anchor filter
                      (set to 0 or None to skip filtering).
    k_score         : neighbourhood size for anchor scoring.
    reference       : index of the reference dataset in ``objects``.
    layer           : layer to draw the shared-space expression from.
    seed            : random seed for the PCA/neighbour steps.

    Returns
    -------
    IntegrationAnchors
    """
    reduction = reduction.lower()
    if reduction not in REDUCTIONS:
        raise ValueError(
            f"Unknown reduction {reduction!r}. Supported: {REDUCTIONS}."
        )
    if len(objects) < 2:
        raise ValueError("find_integration_anchors needs at least two objects.")
    if not 0 <= reference < len(objects):
        raise IndexError(f"reference index {reference} out of range.")

    features = _integration_features(objects, anchor_features, layer)

    # Both reductions start from the per-object scale.data. CCA standardizes it
    # per cell inside _cca; reciprocal PCA runs straight on it (Seurat's
    # per-object RunPCA never column-normalizes for pca-based reductions).
    ref_raw = _anchor_feature_matrix(objects[reference], features, layer)

    rows = []
    weight_embeddings: dict[int, np.ndarray] = {}
    used_dims = min(dims, ref_raw.shape[1] - 1, ref_raw.shape[0])

    for d in range(len(objects)):
        if d == reference:
            continue
        query_raw = _anchor_feature_matrix(objects[d], features, layer)
        used = min(used_dims, query_raw.shape[1] - 1)

        if reduction == "cca":
            # CheckFeatures: RunCCA drops anchor features that are constant in
            # either object before standardizing, so the CCA and its loadings
            # live on this subset.
            keep = _check_features([ref_raw, query_raw])
            emb_ref, emb_query, loadings = _cca(ref_raw[keep], query_raw[keep], used)
            nbrs = _neighbor_sets(emb_ref, emb_query, max(k_anchor, k_score))
            pairs = _mutual_nn_from(nbrs, k_anchor)
            filter_feats = [features[keep[t]] for t in _top_dim_features(loadings)]
            weight_emb = emb_query
        else:  # rpca — reciprocal PCA projections (Seurat's ReciprocalProject)
            # The reciprocal spaces below are only as good as these loadings,
            # and reciprocal PCA standardizes each dimension before the
            # neighbour search (see _pca_loadings), so the trailing PCs have to
            # be right, not just the leading ones. Feeding this the exact SVD
            # loadings takes RPCA anchor agreement with Seurat from 45% to full.
            load_ref = _pca_loadings(ref_raw, used, seed=seed)
            load_query = _pca_loadings(query_raw, used, seed=seed)
            ref_in_ref = ref_raw.T @ load_ref        # ref cells, ref PCA
            query_in_ref = query_raw.T @ load_ref     # query projected into ref PCA
            ref_in_query = ref_raw.T @ load_query     # ref projected into query PCA
            query_in_query = query_raw.T @ load_query  # query cells, query PCA
            # Each object in its own unnormalized PCA — Seurat's plain "pca"
            # reduction, which is what the within-dataset neighbour tables use.
            own_ref, own_query = ref_in_ref, query_in_query

            # Seurat forms each reciprocal space as the stacked [ref; query] and,
            # with l2.norm=TRUE (its default), standardizes every dimension by its
            # SD over the whole stack before L2-normalizing each cell. Skipping
            # that let PC1's dominant variance swamp the neighbour search, so the
            # mutual pairs were wrong — RPCA under-integrated ifnb 4x (batch-mix
            # 0.22 vs Seurat's 0.91). Normalize each space, then split it back
            # into its ref/query halves for the reciprocal MNN.
            n_ref_cells = ref_in_ref.shape[0]
            ref_space = _standardize_and_l2(np.vstack([ref_in_ref, query_in_ref]))
            query_space = _standardize_and_l2(np.vstack([ref_in_query, query_in_query]))
            ref_in_ref, query_in_ref = ref_space[:n_ref_cells], ref_space[n_ref_cells:]
            ref_in_query, query_in_query = query_space[:n_ref_cells], query_space[n_ref_cells:]

            # Reciprocal search: find B(query)-neighbours of A(ref) in the query's
            # PCA space, and A-neighbours of B in the ref's space. The four
            # neighbour tables ScoreAnchors needs are asymmetric here — the
            # within-dataset ones come from each object's *own* PCA (Seurat's
            # nn.reduction stays "pca" on the rpca branch, so it never sees a
            # reciprocal space), the across-dataset ones from the reciprocal
            # projections. Getting the index spaces the wrong way round silently
            # mismatches them and, when n_query > n_ref, runs off the end.
            k_neighbor = max(k_anchor, k_score)
            nbrs = {
                "aa": _nearest(own_ref, own_ref, min(k_neighbor + 1, own_ref.shape[0])),
                "bb": _nearest(own_query, own_query,
                               min(k_neighbor + 1, own_query.shape[0])),
                "ab": _nearest(ref_in_query, query_in_query, k_neighbor),
                "ba": _nearest(query_in_ref, ref_in_ref, k_neighbor),
            }
            pairs = _mutual_nn_from(nbrs, k_anchor)
            filter_feats = None
            weight_emb = query_in_ref

        # FindAnchors filters BEFORE it scores, and the score is rescaled
        # against the 1st/90th percentiles of whatever set it is handed. Scoring
        # first sets those percentiles from anchors that are about to be thrown
        # away, which shifts every surviving score — same ranking, wrong values.
        #
        # Seurat forces k.filter <- NA for reciprocal-PCA (every pca-based
        # nn.reduction), skipping the expression-space filter entirely: the
        # reciprocal projection is itself an expression-space check, and
        # filtering on the shared anchor features here drops good anchors and
        # leaves RPCA under-integrating. Only CCA keeps the filter.
        if k_filter and filter_feats:
            pairs = _filter_anchors(
                pairs,
                _data_matrix(objects[reference], filter_feats),
                _data_matrix(objects[d], filter_feats),
                k_filter,
            )
        n_ref = ref_raw.shape[1]
        scores = _score_anchors(pairs, nbrs, n_ref, k_score)

        weight_embeddings[d] = weight_emb
        for (i, j), s in zip(pairs, scores):
            rows.append((reference, int(i), d, int(j), float(s)))

    anchors = pd.DataFrame(
        rows, columns=["dataset1", "cell1", "dataset2", "cell2", "score"]
    )
    return IntegrationAnchors(
        anchors=anchors,
        objects=objects,
        reference=reference,
        reduction=reduction,
        anchor_features=features,
        dims=used_dims,
        weight_embeddings=weight_embeddings,
    )

select_integration_features

select_integration_features(objects: list, nfeatures: int = 2000, assay: list[str] | None = None, fvf_nfeatures: int = 2000) -> list[str]

Rank genes by how many datasets call them variable (Seurat's SelectIntegrationFeatures).

Mirrors SelectIntegrationFeatures(object.list, nfeatures = 2000). Every object's variable features are pooled and counted, genes missing from any object are dropped, and the nfeatures counted most often are kept. Genes tied on that count, including the ones the cut falls among, go in order of the median of their rank across the lists that hold them. A tie on both goes by name. R orders names by the session's collation and this orders them by code point, which is what R does under LC_COLLATE=C, the locale Rscript runs in.

An object with no variable features gets them from find_variable_features(nfeatures=fvf_nfeatures) run on a copy, as in Seurat, so the objects passed in are left as they are.

Parameters:

  • objects (list) –

    the Truecell objects to be integrated.

  • nfeatures (int, default: 2000 ) –

    how many features to return.

  • assay (list[str] | None, default: None ) –

    one assay name per object (default: each object's active assay).

  • fvf_nfeatures (int, default: 2000 ) –

    variable features to compute for an object that has none.

Returns:

  • list[str] –

    Feature names, those called variable by the most datasets first.

Source code in truecell/anchors.py
def select_integration_features(
    objects: list,
    nfeatures: int = 2000,
    assay: list[str] | None = None,
    fvf_nfeatures: int = 2000,
) -> list[str]:
    """Rank genes by how many datasets call them variable (Seurat's ``SelectIntegrationFeatures``).

    Mirrors ``SelectIntegrationFeatures(object.list, nfeatures = 2000)``. Every
    object's variable features are pooled and counted, genes missing from any
    object are dropped, and the ``nfeatures`` counted most often are kept. Genes
    tied on that count, including the ones the cut falls among, go in order of the
    median of their rank across the lists that hold them. A tie on both goes by
    name. R orders names by the session's collation and this orders them by code
    point, which is what R does under ``LC_COLLATE=C``, the locale ``Rscript``
    runs in.

    An object with no variable features gets them from
    ``find_variable_features(nfeatures=fvf_nfeatures)`` run on a copy, as in
    Seurat, so the objects passed in are left as they are.

    Parameters
    ----------
    objects       : the Truecell objects to be integrated.
    nfeatures     : how many features to return.
    assay         : one assay name per object (default: each object's active assay).
    fvf_nfeatures : variable features to compute for an object that has none.

    Returns
    -------
    list[str]
        Feature names, those called variable by the most datasets first.
    """
    import copy

    from .preprocessing import find_variable_features

    if assay is not None and len(assay) != len(objects):
        raise ValueError("Give one assay per object, or none.")
    lists, present = [], []
    for i, obj in enumerate(objects):
        assay_name = assay[i] if assay is not None else obj.active_assay
        variable = list(obj.assays[assay_name].variable_features)
        if not variable:
            work = copy.deepcopy(obj)
            find_variable_features(work, assay=assay_name, nfeatures=fvf_nfeatures)
            variable = list(work.assays[assay_name].variable_features)
        lists.append(variable)
        present.append(set(obj.assays[assay_name].features()))

    counts: dict[str, int] = {}
    for variable in lists:
        for feature in variable:
            counts[feature] = counts.get(feature, 0) + 1
    # sort(table(var.features), decreasing = TRUE). table() puts names in collation
    # order, and R's sort by count is stable, so a count keeps that order.
    ranked = sorted(sorted(counts), key=lambda f: -counts[f])
    ranked = [f for f in ranked if all(f in names for names in present)]
    if not ranked:
        return []
    tie_val = counts[ranked[min(nfeatures, len(ranked)) - 1]]

    positions = []
    for variable in lists:
        position: dict[str, int] = {}
        for rank, feature in enumerate(variable, start=1):
            position.setdefault(feature, rank)
        positions.append(position)

    def median_rank(feature: str) -> float:
        return float(np.median([p[feature] for p in positions if feature in p]))

    above = sorted((f for f in ranked if counts[f] > tie_val), key=median_rank)
    tied = sorted((f for f in ranked if counts[f] == tie_val), key=median_rank)
    return above + tied[: nfeatures - len(above)]

integrate_embeddings

integrate_embeddings(anchors: IntegrationAnchors, reduction, new_reduction: str = 'integrated_dr', dims_to_integrate: Optional[list[int]] = None, k_weight: int = 100, sd_weight: float = 1.0) -> DimReduc

Batch-correct an existing reduction (Seurat's IntegrateEmbeddings).

The v5 counterpart to integrate_data, and a genuinely different algorithm rather than a wrapper over it. IntegrateData corrects expression and leaves you to re-scale and re-run PCA; IntegrateEmbeddings corrects the embedding itself, so the output lives in the input reduction's basis and keeps its loadings.

Seurat implements it by transposing the embedding into a fake assay whose "features" are the dimensions (drtointegrate-1 …) and pushing that through the very same anchor machinery, which is why this shares _anchor_weights with integrate_data. The one substantive difference is the weight space: RunIntegration's dims = NULL branch hands FindWeights the drtointegrate matrix itself, so neighbours are measured in the uncorrected embedding — not in the fresh per-pair PCA that the expression path builds.

Parameters:

  • anchors (IntegrationAnchors) –
  • reduction –

    a DimReduc covering every cell in anchors.objects — the reduction to correct.

  • new_reduction (str, default: 'integrated_dr' ) –

    key for the returned reduction.

  • dims_to_integrate (Optional[list[int]], default: None ) –

    which dimensions to correct (0-indexed; default all).

  • k_weight (int, default: 100 ) –

    anchors used to weight each query cell's correction.

  • sd_weight (float, default: 1.0 ) –

    bandwidth of the anchor kernel; enters as (2/sd)².

Returns:

  • DimReduc –

    The corrected embedding, cells in merged order (reference first).

Source code in truecell/anchors.py
def integrate_embeddings(
    anchors: IntegrationAnchors,
    reduction,
    new_reduction: str = "integrated_dr",
    dims_to_integrate: Optional[list[int]] = None,
    k_weight: int = 100,
    sd_weight: float = 1.0,
) -> DimReduc:
    """Batch-correct an existing reduction (Seurat's ``IntegrateEmbeddings``).

    The v5 counterpart to :func:`integrate_data`, and a genuinely different
    algorithm rather than a wrapper over it. ``IntegrateData`` corrects
    *expression* and leaves you to re-scale and re-run PCA; ``IntegrateEmbeddings``
    corrects the **embedding itself**, so the output lives in the input
    reduction's basis and keeps its loadings.

    Seurat implements it by transposing the embedding into a fake assay whose
    "features" are the dimensions (``drtointegrate-1 …``) and pushing that
    through the very same anchor machinery, which is why this shares
    :func:`_anchor_weights` with :func:`integrate_data`. The one substantive
    difference is the weight space: ``RunIntegration``'s ``dims = NULL`` branch
    hands ``FindWeights`` the ``drtointegrate`` matrix itself, so neighbours are
    measured in the *uncorrected embedding* — not in the fresh per-pair PCA
    that the expression path builds.

    Parameters
    ----------
    anchors           : an :class:`IntegrationAnchors` from
                        :func:`find_integration_anchors`.
    reduction         : a :class:`DimReduc` covering every cell in
                        ``anchors.objects`` — the reduction to correct.
    new_reduction     : key for the returned reduction.
    dims_to_integrate : which dimensions to correct (0-indexed; default all).
    k_weight          : anchors used to weight each query cell's correction.
    sd_weight         : bandwidth of the anchor kernel; enters as ``(2/sd)²``.

    Returns
    -------
    DimReduc
        The corrected embedding, cells in merged order (reference first).
    """
    objects = anchors.objects
    ref = anchors.reference

    emb = np.asarray(reduction.cell_embeddings)
    pos = {c: i for i, c in enumerate(reduction.cells())}
    dims = list(range(emb.shape[1])) if dims_to_integrate is None \
        else list(dims_to_integrate)

    missing = [c for o in objects for c in o.cell_names() if c not in pos]
    if missing:
        raise ValueError(
            f"{len(missing)} cell(s) in the anchor objects are absent from "
            f"reduction {reduction.key!r} (first: {missing[0]!r}). "
            "IntegrateEmbeddings needs a reduction spanning every dataset."
        )

    def block(d):
        """The dataset's embedding as dims × cells — Seurat's drtointegrate."""
        cells = objects[d].cell_names()
        return emb[[pos[c] for c in cells]][:, dims].T

    order = [ref] + [d for d in range(len(objects)) if d != ref]
    ref_block = block(ref)
    blocks = {ref: ref_block}

    for d in order[1:]:
        query_block = block(d)
        pair = anchors.anchors[anchors.anchors["dataset2"] == d]
        if len(pair) == 0:
            blocks[d] = query_block
            continue
        i_idx = pair["cell1"].to_numpy()
        j_idx = pair["cell2"].to_numpy()
        bv = ref_block[:, i_idx] - query_block[:, j_idx]  # dims × n_anchor
        weights = _anchor_weights(
            query_block.T, j_idx, pair["score"].to_numpy(), k_weight, sd_weight
        )
        blocks[d] = query_block + bv @ weights

    corrected = np.hstack([blocks[d] for d in order]).T  # cells × dims
    cells = [c for d in order for c in objects[d].cell_names()]
    loadings = None
    if reduction.feature_loadings is not None and len(reduction.feature_loadings):
        loadings = np.asarray(reduction.feature_loadings)[:, dims]
    key = f"{new_reduction.replace('_', '').replace('.', '')}_"
    return DimReduc(
        cell_embeddings=corrected,
        feature_loadings=loadings,
        assay_used=reduction.assay_used,
        key=key,
        cell_names=cells,
        feature_names=(reduction.features() if loadings is not None else None),
    )

integrate_data

integrate_data(anchors: IntegrationAnchors, new_assay: str = 'integrated', k_weight: int = 100, sd_weight: float = 1.0, add_cell_ids: Optional[list[str]] = None, seed: int = 42) -> 'object'

Batch-correct query datasets onto the reference (Seurat's IntegrateData).

Mirrors IntegrateData(anchors). For every query dataset, each cell is corrected by a distance-weighted sum of anchor correction vectors (expr_ref − expr_query); the reference is left unchanged. The corrected expression of the anchor features is stored as the data layer of a new "integrated" assay on a merged object, which becomes the active assay.

Downstream: scale_data + run_pca on the integrated assay yields an embedding that clusters by cell type rather than by batch.

Parameters:

  • anchors (IntegrationAnchors) –
  • new_assay (str, default: 'integrated' ) –

    name for the corrected assay (default "integrated").

  • k_weight (int, default: 100 ) –

    anchors used to weight each query cell's correction. Counts anchors, not anchor cells — see _anchor_weights.

  • sd_weight (float, default: 1.0 ) –

    bandwidth of the anchor kernel; enters as (2/sd_weight)².

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

    optional per-object prefixes for the merged cell names.

  • seed (int, default: 42 ) –

    random seed for the per-pair weight PCA.

Returns:

  • Truecell –

    A merged object carrying the new_assay assay (active) alongside the original assay.

Source code in truecell/anchors.py
def integrate_data(
    anchors: IntegrationAnchors,
    new_assay: str = "integrated",
    k_weight: int = 100,
    sd_weight: float = 1.0,
    add_cell_ids: Optional[list[str]] = None,
    seed: int = 42,
) -> "object":
    """Batch-correct query datasets onto the reference (Seurat's ``IntegrateData``).

    Mirrors ``IntegrateData(anchors)``. For every query dataset, each cell is
    corrected by a distance-weighted sum of anchor correction vectors
    (``expr_ref − expr_query``); the reference is left unchanged. The corrected
    expression of the anchor features is stored as the ``data`` layer of a new
    ``"integrated"`` assay on a merged object, which becomes the active assay.

    Downstream: ``scale_data`` + ``run_pca`` on the integrated assay yields an
    embedding that clusters by cell type rather than by batch.

    Parameters
    ----------
    anchors      : an :class:`IntegrationAnchors` from
                   :func:`find_integration_anchors`.
    new_assay    : name for the corrected assay (default ``"integrated"``).
    k_weight     : anchors used to weight each query cell's correction. Counts
                   anchors, not anchor cells — see :func:`_anchor_weights`.
    sd_weight    : bandwidth of the anchor kernel; enters as ``(2/sd_weight)²``.
    add_cell_ids : optional per-object prefixes for the merged cell names.
    seed         : random seed for the per-pair weight PCA.

    Returns
    -------
    Truecell
        A merged object carrying the ``new_assay`` assay (active) alongside the
        original assay.
    """
    from .assay import Assay

    objects = anchors.objects
    ref = anchors.reference
    features = anchors.anchor_features

    # Merge order: reference first, then the remaining datasets in list order.
    order = [ref] + [d for d in range(len(objects)) if d != ref]
    ref_obj = objects[ref]
    others = [objects[d] for d in order[1:]]

    if add_cell_ids is not None:
        ordered_ids = [add_cell_ids[d] for d in order]
    else:
        ordered_ids = None

    merged = ref_obj.merge(others, add_cell_ids=ordered_ids)

    ref_data = _data_matrix(ref_obj, features)  # features × cells_ref (unchanged)
    corrected_blocks = [ref_data]

    for d in order[1:]:
        query_data = _data_matrix(objects[d], features)  # features × cells_q
        pair = anchors.anchors[anchors.anchors["dataset2"] == d]

        if len(pair) == 0:
            # No anchors to this dataset — leave it uncorrected.
            corrected_blocks.append(query_data)
            continue

        i_idx = pair["cell1"].to_numpy()
        j_idx = pair["cell2"].to_numpy()
        anchor_scores = pair["score"].to_numpy()

        # Correction vectors in feature space: reference minus query at anchors.
        bv = ref_data[:, i_idx] - query_data[:, j_idx]  # features × n_anchor

        # The weights live in a PCA of *this pair*, not in the anchor space.
        # RunIntegration merges the reference and query, re-scales on the anchor
        # features and runs a fresh PCA, and FindWeights searches there. Reusing
        # the CCA embedding instead measures distances in a space built to make
        # the batches overlap, which is not the same neighbourhood.
        query_emb = _pair_weight_embedding(
            ref_obj, objects[d], features, anchors.dims, seed
        )
        weights = _anchor_weights(
            query_emb, j_idx, anchor_scores, k_weight, sd_weight
        )
        correction = bv @ weights           # features × cells_q
        corrected_blocks.append(query_data + correction)

    integrated = np.hstack(corrected_blocks)  # features × total_cells
    cell_names = merged.cell_names()

    integrated_assay = Assay(
        data=integrated,
        feature_names=list(features),
        cell_names=list(cell_names),
        var_features=list(features),
        key=f"{new_assay.lower()}_",
    )
    merged.assays[new_assay] = integrated_assay
    merged.active_assay = new_assay
    return merged

IntegrationAnchors

IntegrationAnchors(anchors: DataFrame, objects: list, reference: int, reduction: str, anchor_features: list[str], dims: int, weight_embeddings: dict[int, ndarray])

Anchors linking a reference dataset to one or more query datasets.

Slots
  • anchors — DataFrame with columns dataset1, cell1, dataset2, cell2, score. dataset1 is always the reference; the cell columns hold within-dataset 0-based row indices.
  • objects — the list of Truecell objects passed to find_integration_anchors (order preserved).
  • reference — index into objects of the reference dataset.
  • reduction — "cca" or "rpca" — how the shared space was built.
  • anchor_features — the features the anchors (and correction) run on.
  • dims — number of shared dimensions used.
  • weight_embeddings — {query_index: (n_query_cells × dims) array} — each query dataset's cells in the shared anchor space. Kept for inspection; integrate_data does not weight with it, because Seurat weights in a fresh PCA of the merged pair instead.
Source code in truecell/anchors.py
def __init__(
    self,
    anchors: pd.DataFrame,
    objects: list,
    reference: int,
    reduction: str,
    anchor_features: list[str],
    dims: int,
    weight_embeddings: dict[int, np.ndarray],
) -> None:
    self.anchors = anchors
    self.objects = objects
    self.reference = reference
    self.reduction = reduction
    self.anchor_features = list(anchor_features)
    self.dims = dims
    self.weight_embeddings = weight_embeddings

Reference mapping

find_transfer_anchors

find_transfer_anchors(reference, query, anchor_features: Optional[list[str]] = None, reduction: str = 'pcaproject', dims: int = 30, k_anchor: int = 5, k_filter: int = 200, k_score: int = 30, layer: str = 'scale.data', seed: int = 42) -> TransferAnchors

Find transfer anchors from a reference to a query (Seurat's FindTransferAnchors).

Mirrors FindTransferAnchors(reference, query, reduction = "pcaproject"). Anchors are mutual nearest neighbours between the reference and the query in a shared space — by default the reference's own PCA, into which the query is projected — scored by neighbourhood consistency and filtered against the original expression space.

Parameters:

  • reference –

    annotated reference Truecell object (normalized + with variable features / scaled data).

  • query –

    query Truecell object to annotate (same preprocessing).

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

    features to anchor on (default: variable features shared by reference and query).

  • reduction (str, default: 'pcaproject' ) –

    "pcaproject" (project the query into the reference's PCA; the default and most robust for annotation) or "cca" (a jointly-learned space, for harder cross-modality/species cases). The reference PCA is computed on the shared anchor features, so the reference need not already carry a pca reduction.

  • dims (int, default: 30 ) –

    number of shared dimensions to use.

  • k_anchor (int, default: 5 ) –

    neighbours for the mutual-nearest-neighbour search.

  • k_filter (int, default: 200 ) –

    neighbourhood size for the feature-space anchor filter (set to 0 or None to skip filtering).

  • k_score (int, default: 30 ) –

    neighbourhood size for anchor scoring.

  • layer (str, default: 'scale.data' ) –

    layer to draw the shared-space expression from.

  • seed (int, default: 42 ) –

    random seed for the PCA/neighbour steps.

Returns:

Source code in truecell/transfer.py
def find_transfer_anchors(
    reference,
    query,
    anchor_features: Optional[list[str]] = None,
    reduction: str = "pcaproject",
    dims: int = 30,
    k_anchor: int = 5,
    k_filter: int = 200,
    k_score: int = 30,
    layer: str = "scale.data",
    seed: int = 42,
) -> TransferAnchors:
    """Find transfer anchors from a reference to a query (Seurat's ``FindTransferAnchors``).

    Mirrors ``FindTransferAnchors(reference, query, reduction = "pcaproject")``.
    Anchors are mutual nearest neighbours between the reference and the query in
    a shared space — by default the reference's own PCA, into which the query is
    projected — scored by neighbourhood consistency and filtered against the
    original expression space.

    Parameters
    ----------
    reference       : annotated reference Truecell object (normalized + with
                      variable features / scaled data).
    query           : query Truecell object to annotate (same preprocessing).
    anchor_features : features to anchor on (default: variable features shared
                      by reference and query).
    reduction       : ``"pcaproject"`` (project the query into the reference's
                      PCA; the default and most robust for annotation) or
                      ``"cca"`` (a jointly-learned space, for harder
                      cross-modality/species cases). The reference PCA is
                      computed on the shared anchor features, so the reference
                      need not already carry a ``pca`` reduction.
    dims            : number of shared dimensions to use.
    k_anchor        : neighbours for the mutual-nearest-neighbour search.
    k_filter        : neighbourhood size for the feature-space anchor filter
                      (set to 0 or None to skip filtering).
    k_score         : neighbourhood size for anchor scoring.
    layer           : layer to draw the shared-space expression from.
    seed            : random seed for the PCA/neighbour steps.

    Returns
    -------
    TransferAnchors
    """
    reduction = reduction.lower()
    if reduction not in REDUCTIONS:
        raise ValueError(
            f"Unknown reduction {reduction!r}. Supported: {REDUCTIONS}."
        )

    features = _integration_features([reference, query], anchor_features)

    ref_scaled = _l2_normalize_cols(
        _anchor_feature_matrix(reference, features, layer)
    )
    query_scaled = _l2_normalize_cols(
        _anchor_feature_matrix(query, features, layer)
    )
    if ref_scaled.shape[0] != query_scaled.shape[0]:
        raise ValueError(
            "Reference and query disagree on the number of anchor features "
            f"({ref_scaled.shape[0]} vs {query_scaled.shape[0]}); ensure the "
            "anchor features are scaled in both objects."
        )

    used = min(
        dims,
        ref_scaled.shape[1] - 1,
        query_scaled.shape[1] - 1,
        ref_scaled.shape[0],
    )

    if reduction == "pcaproject":
        # Project the query through the reference's PCA loadings — both datasets
        # end up in the reference's principal-component space.
        load_ref = _pca_loadings(ref_scaled, used, seed=seed)
        ref_emb = _l2_normalize_rows(ref_scaled.T @ load_ref)
        query_emb = _l2_normalize_rows(query_scaled.T @ load_ref)
    else:  # cca — a jointly-learned shared space
        ref_emb, query_emb, _loadings = _cca(ref_scaled, query_scaled, used)

    # FindTransferAnchors goes through the same FindAnchors internals as
    # FindIntegrationAnchors, so it gets the same four-way neighbour tables.
    nbrs = _neighbor_sets(ref_emb, query_emb, max(k_anchor, k_score))
    pairs = _mutual_nn_from(nbrs, k_anchor)
    n_ref = ref_emb.shape[0]
    # Filter first: the score is rescaled against the percentiles of the set it
    # is handed, so scoring before filtering shifts every surviving score.
    if k_filter:
        pairs = _filter_anchors(pairs, ref_scaled, query_scaled, k_filter)
    scores = _score_anchors(pairs, nbrs, n_ref, k_score)

    rows = [(int(i), int(j), float(s)) for (i, j), s in zip(pairs, scores)]
    anchors = pd.DataFrame(rows, columns=["cell1", "cell2", "score"])

    return TransferAnchors(
        anchors=anchors,
        reference=reference,
        query=query,
        reduction=reduction,
        anchor_features=features,
        dims=used,
        query_embedding=query_emb,
    )

transfer_data

transfer_data(anchors: TransferAnchors, refdata: Union[str, ndarray, list, Series], k_weight: int = 50, sd_weight: float = 1.0, refdata_features: Optional[list[str]] = None) -> DataFrame

Transfer labels or expression from reference to query (Seurat's TransferData).

Mirrors TransferData(anchorset, refdata = "celltype"). Each query cell gets a weight over the anchors — a distance-weighted, anchor-score-scaled Gaussian kernel in the shared space (the same weighting IntegrateData uses) — and the reference information is carried across those weights.

refdata selects the mode:

  • classification — a metadata column name (str) or a 1-D array of per-reference-cell labels. Returns a DataFrame indexed by query cell with predicted.id, one prediction.score.<class> column per reference class (each query cell's rows sum to 1), and prediction.score.max.
  • imputation — a 2-D features × reference-cells matrix. Returns a DataFrame of predicted query expression (features × query cells); name the rows with refdata_features.

Parameters:

  • anchors (TransferAnchors) –
  • refdata (Union[str, ndarray, list, Series]) –

    reference labels (str column / 1-D array) or a 2-D features × reference-cells matrix to impute.

  • k_weight (int, default: 50 ) –

    anchors used to weight each query cell.

  • sd_weight (float, default: 1.0 ) –

    bandwidth multiplier for the Gaussian anchor kernel.

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

    row names for the imputation output (2-D refdata).

Returns:

  • DataFrame –
Source code in truecell/transfer.py
def transfer_data(
    anchors: TransferAnchors,
    refdata: Union[str, np.ndarray, list, pd.Series],
    k_weight: int = 50,
    sd_weight: float = 1.0,
    refdata_features: Optional[list[str]] = None,
) -> pd.DataFrame:
    """Transfer labels or expression from reference to query (Seurat's ``TransferData``).

    Mirrors ``TransferData(anchorset, refdata = "celltype")``. Each query cell
    gets a weight over the anchors — a distance-weighted, anchor-score-scaled
    Gaussian kernel in the shared space (the same weighting ``IntegrateData``
    uses) — and the reference information is carried across those weights.

    ``refdata`` selects the mode:

    * **classification** — a metadata column name (``str``) or a 1-D array of
      per-reference-cell labels. Returns a DataFrame indexed by query cell with
      ``predicted.id``, one ``prediction.score.<class>`` column per reference
      class (each query cell's rows sum to 1), and ``prediction.score.max``.
    * **imputation** — a 2-D ``features × reference-cells`` matrix. Returns a
      DataFrame of predicted query expression (features × query cells); name the
      rows with ``refdata_features``.

    Parameters
    ----------
    anchors          : a :class:`TransferAnchors` from
                       :func:`find_transfer_anchors`.
    refdata          : reference labels (str column / 1-D array) or a 2-D
                       ``features × reference-cells`` matrix to impute.
    k_weight         : anchors used to weight each query cell.
    sd_weight        : bandwidth multiplier for the Gaussian anchor kernel.
    refdata_features : row names for the imputation output (2-D ``refdata``).

    Returns
    -------
    pandas.DataFrame
    """
    anchor_df = anchors.anchors
    if len(anchor_df) == 0:
        raise ValueError(
            "No anchors between reference and query; cannot transfer. Try a "
            "larger k_anchor or k_filter=0 in find_transfer_anchors."
        )

    query_cells = anchors.query.cell_names()
    ref_cell1 = anchor_df["cell1"].to_numpy()
    query_cell2 = anchor_df["cell2"].to_numpy()
    anchor_scores = anchor_df["score"].to_numpy()

    weights = _anchor_weight_matrix(
        anchors.query_embedding, query_cell2, anchor_scores, k_weight, sd_weight
    )  # (n_query × n_anchor)

    classification = isinstance(refdata, str) or np.ndim(refdata) == 1
    if classification:
        return _transfer_labels(
            anchors.reference, refdata, ref_cell1, weights, query_cells
        )
    return _transfer_expression(
        refdata, refdata_features, ref_cell1, weights, query_cells
    )

TransferAnchors

TransferAnchors(anchors: DataFrame, reference, query, reduction: str, anchor_features: list[str], dims: int, query_embedding: ndarray)

Anchors linking a fixed reference to a query, for label/data transfer.

Slots
  • anchors — DataFrame with columns cell1, cell2, score. cell1 is a within-reference 0-based cell index, cell2 a within-query one; the reference is never moved.
  • reference — the reference Truecell object (annotated atlas).
  • query — the query Truecell object (to be annotated).
  • reduction — "pcaproject" or "cca" — how the shared space was built.
  • anchor_features — the features the anchors run on.
  • dims — number of shared dimensions used.
  • query_embedding — (n_query_cells × dims) — the query cells in the shared space, used by transfer_data to weight anchors.
Source code in truecell/transfer.py
def __init__(
    self,
    anchors: pd.DataFrame,
    reference,
    query,
    reduction: str,
    anchor_features: list[str],
    dims: int,
    query_embedding: np.ndarray,
) -> None:
    self.anchors = anchors
    self.reference = reference
    self.query = query
    self.reduction = reduction
    self.anchor_features = list(anchor_features)
    self.dims = dims
    self.query_embedding = query_embedding

map_query

map_query(anchors: TransferAnchors, refdata: Optional[Union[str, ndarray, list, Series]] = None, reference_reduction: str = 'pca', reduction_model: str = 'umap', reduction_name: str = 'ref.umap', reduction_key: str = 'refUMAP_', k_weight: int = 50, sd_weight: float = 1.0, refdata_features: Optional[list[str]] = None, layer: str = 'scale.data') -> Optional[DataFrame]

Annotate and place a query in a reference (Seurat's MapQuery).

Mirrors MapQuery(anchorset, query, reference, refdata = "celltype"). The single call that turns transfer anchors into a mapped query:

  1. transfer_data carries refdata across the anchors, and — for a categorical label — writes predicted.id / prediction.score.* straight onto query.meta_data.
  2. project_umap projects the query into the reference's UMAP, stored as query.reductions[reduction_name].

Both steps mutate the query object (the anchors' query) in place.

Parameters:

  • anchors (TransferAnchors) –
  • refdata (Optional[Union[str, ndarray, list, Series]], default: None ) –

    reference labels to transfer (metadata column name or a per-reference-cell array) or a 2-D features × reference-cells matrix to impute. Pass None to skip transfer and only project the UMAP.

  • reference_reduction (str, default: 'pca' ) –

    reference reduction whose loadings project the query into the UMAP's input space (default "pca").

  • reduction_model (str, default: 'umap' ) –

    reference reduction holding the fitted UMAP model.

  • reduction_name (str, default: 'ref.umap' ) –

    storage key for the projection in query.reductions.

  • reduction_key (str, default: 'refUMAP_' ) –

    prefix for the projected dimension names.

  • k_weight (int, default: 50 ) –

    anchor-weighting knobs passed to transfer_data.

  • sd_weight (int, default: 50 ) –

    anchor-weighting knobs passed to transfer_data.

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

    row names for imputation output (2-D refdata).

  • layer (str, default: 'scale.data' ) –

    layer to draw the query's expression from.

Returns:

  • DataFrame or None –

    The transferred predictions (classification) or imputed expression (imputation); None when refdata is not given.

Source code in truecell/mapping.py
def map_query(
    anchors: TransferAnchors,
    refdata: Optional[Union[str, np.ndarray, list, pd.Series]] = None,
    reference_reduction: str = "pca",
    reduction_model: str = "umap",
    reduction_name: str = "ref.umap",
    reduction_key: str = "refUMAP_",
    k_weight: int = 50,
    sd_weight: float = 1.0,
    refdata_features: Optional[list[str]] = None,
    layer: str = "scale.data",
) -> Optional[pd.DataFrame]:
    """Annotate and place a query in a reference (Seurat's ``MapQuery``).

    Mirrors ``MapQuery(anchorset, query, reference, refdata = "celltype")``. The
    single call that turns transfer anchors into a mapped query:

    1. :func:`~truecell.transfer_data` carries ``refdata`` across the anchors, and
       — for a categorical label — writes ``predicted.id`` /
       ``prediction.score.*`` straight onto ``query.meta_data``.
    2. :func:`project_umap` projects the query into the reference's UMAP, stored
       as ``query.reductions[reduction_name]``.

    Both steps mutate the query object (the anchors' ``query``) in place.

    Parameters
    ----------
    anchors             : a :class:`~truecell.TransferAnchors` from
                          :func:`~truecell.find_transfer_anchors`.
    refdata             : reference labels to transfer (metadata column name or a
                          per-reference-cell array) or a 2-D
                          ``features × reference-cells`` matrix to impute. Pass
                          ``None`` to skip transfer and only project the UMAP.
    reference_reduction : reference reduction whose loadings project the query
                          into the UMAP's input space (default ``"pca"``).
    reduction_model     : reference reduction holding the fitted UMAP model.
    reduction_name      : storage key for the projection in ``query.reductions``.
    reduction_key       : prefix for the projected dimension names.
    k_weight, sd_weight : anchor-weighting knobs passed to
                          :func:`~truecell.transfer_data`.
    refdata_features    : row names for imputation output (2-D ``refdata``).
    layer               : layer to draw the query's expression from.

    Returns
    -------
    pandas.DataFrame or None
        The transferred predictions (classification) or imputed expression
        (imputation); ``None`` when ``refdata`` is not given.
    """
    query = anchors.query

    predictions: Optional[pd.DataFrame] = None
    if refdata is not None:
        predictions = transfer_data(
            anchors,
            refdata,
            k_weight=k_weight,
            sd_weight=sd_weight,
            refdata_features=refdata_features,
        )
        classification = isinstance(refdata, str) or np.ndim(refdata) == 1
        if classification:
            # Write predicted.id / prediction.score.* onto the query metadata,
            # as Seurat's MapQuery does. Imputation output is returned, not stored.
            for col in predictions.columns:
                query.meta_data[col] = (
                    predictions[col].reindex(query.meta_data.index).to_numpy()
                )

    project_umap(
        query,
        anchors.reference,
        reduction=reference_reduction,
        umap_reduction=reduction_model,
        reduction_name=reduction_name,
        reduction_key=reduction_key,
        layer=layer,
    )
    return predictions

project_umap

project_umap(query, reference, reduction: str = 'pca', umap_reduction: str = 'umap', dims: Optional[Union[list[int], range]] = None, reduction_name: str = 'ref.umap', reduction_key: str = 'refUMAP_', layer: str = 'scale.data') -> DimReduc

Project a query into a reference's UMAP (Seurat's ProjectUMAP).

Mirrors ProjectUMAP(query, reference, reduction.model = "umap"). The query is first projected into the reference's PCA (through the reference's loadings), then run through the reference's fitted UMAP model in transform-only mode — so the query cells land in the reference's existing embedding rather than in a fresh, unrelated one. Stores the result as query.reductions[reduction_name] and returns it.

The reference must already carry both a PCA reduction (reduction, for the loadings) and a UMAP reduction (umap_reduction) fitted with a returnable model — i.e. run_umap(reference), which stashes the umap-learn model in reduction.misc["umap_model"].

Parameters:

  • query –

    query Truecell object (normalized + scaled on the reference's PCA features).

  • reference –

    reference Truecell object carrying the fitted PCA + UMAP.

  • reduction (str, default: 'pca' ) –

    reference reduction whose loadings project the query (default "pca").

  • umap_reduction (str, default: 'umap' ) –

    reference reduction holding the fitted UMAP model (default "umap").

  • dims (Optional[Union[list[int], range]], default: None ) –

    which PCA dimensions to feed the UMAP model (0-indexed). Defaults to the dimensions the model was trained on.

  • reduction_name (str, default: 'ref.umap' ) –

    storage key for the projection in query.reductions.

  • reduction_key (str, default: 'refUMAP_' ) –

    prefix for the projected dimension names.

  • layer (str, default: 'scale.data' ) –

    layer to draw the query's expression from.

Returns:

  • DimReduc –

    The query's cells in the reference UMAP; also stored on query.

Source code in truecell/mapping.py
def project_umap(
    query,
    reference,
    reduction: str = "pca",
    umap_reduction: str = "umap",
    dims: Optional[Union[list[int], range]] = None,
    reduction_name: str = "ref.umap",
    reduction_key: str = "refUMAP_",
    layer: str = "scale.data",
) -> DimReduc:
    """Project a query into a reference's UMAP (Seurat's ``ProjectUMAP``).

    Mirrors ``ProjectUMAP(query, reference, reduction.model = "umap")``. The
    query is first projected into the reference's PCA (through the reference's
    loadings), then run through the reference's *fitted* UMAP model in
    transform-only mode — so the query cells land in the reference's existing
    embedding rather than in a fresh, unrelated one. Stores the result as
    ``query.reductions[reduction_name]`` and returns it.

    The reference must already carry both a PCA reduction (``reduction``, for the
    loadings) and a UMAP reduction (``umap_reduction``) fitted with a returnable
    model — i.e. ``run_umap(reference)``, which stashes the ``umap-learn`` model
    in ``reduction.misc["umap_model"]``.

    Parameters
    ----------
    query          : query Truecell object (normalized + scaled on the reference's
                     PCA features).
    reference      : reference Truecell object carrying the fitted PCA + UMAP.
    reduction      : reference reduction whose loadings project the query
                     (default ``"pca"``).
    umap_reduction : reference reduction holding the fitted UMAP model (default
                     ``"umap"``).
    dims           : which PCA dimensions to feed the UMAP model (0-indexed).
                     Defaults to the dimensions the model was trained on.
    reduction_name : storage key for the projection in ``query.reductions``.
    reduction_key  : prefix for the projected dimension names.
    layer          : layer to draw the query's expression from.

    Returns
    -------
    DimReduc
        The query's cells in the reference UMAP; also stored on ``query``.
    """
    if reduction not in reference.reductions:
        raise KeyError(
            f"Reference reduction {reduction!r} not found; run run_pca(reference) "
            "first."
        )
    if umap_reduction not in reference.reductions:
        raise KeyError(
            f"Reference reduction {umap_reduction!r} not found; run "
            "run_umap(reference) first."
        )

    umap_dr = reference.reductions[umap_reduction]
    model = umap_dr.misc.get("umap_model")
    if model is None:
        raise ValueError(
            f"Reference reduction {umap_reduction!r} has no fitted UMAP model in "
            "misc['umap_model']; run_umap(reference) must be run from a reduction "
            "(not a graph) so the model is stored for transform-only projection."
        )

    query_pca = _project_into_reference_pca(query, reference, reduction, layer)
    query_pca = _select_model_dims(query_pca, model, dims)

    umap_coords = np.asarray(model.transform(query_pca))

    dim_names = [f"{reduction_key}{i + 1}" for i in range(umap_coords.shape[1])]
    projected = DimReduc(
        cell_embeddings=umap_coords,
        cell_names=query.cell_names(),
        feature_names=dim_names,
        assay_used=query.active_assay,
        key=reduction_key,
        misc={
            "projected_from": umap_reduction,
            "reference_reduction": reduction,
            "query_pca": query_pca,
        },
    )
    query.reductions[reduction_name] = projected
    return projected