scPTR / src /scptr /tools /_gamma.py
bryan7264's picture
Add files using upload-large-folder tool
925ee3b verified
Raw
History Blame Contribute Delete
4.47 kB
"""Per-cell degradation rate (gamma) estimation."""
from __future__ import annotations
import logging
import numpy as np
from anndata import AnnData
from .._constants import (
SMOOTHED_UNSPLICED,
SMOOTHED_SPLICED,
BETA,
GAMMA,
DEFAULT_CLIP_QUANTILE,
)
from .._utils import get_layer, require_layers, require_var, log_params
from .._numba_kernels import _compute_gamma_kernel
logger = logging.getLogger("scptr")
def estimate_gamma(
adata: AnnData,
clip_quantile: float = DEFAULT_CLIP_QUANTILE,
mode: str = "steady_state",
velocity_layer: str | None = None,
min_spliced: float = 0.01,
) -> None:
"""Compute per-cell, per-gene mRNA degradation rate gamma.
Parameters
----------
adata
Annotated data matrix with smoothed layers and ``var['beta']``.
clip_quantile
Upper quantile for per-gene clipping (default 0.99).
mode
``'steady_state'`` (default): ``gamma = beta * u / s``.
``'dynamic'``: ``gamma = (beta * u - ds/dt) / s``, using the
full ODE instead of the steady-state assumption.
velocity_layer
Layer name containing the ``ds/dt`` estimate, required when
``mode='dynamic'``.
min_spliced
Minimum smoothed spliced count for reliable gamma estimation.
Cells with ``Ms < min_spliced`` for a given gene get gamma=0.
"""
require_layers(adata, SMOOTHED_UNSPLICED, SMOOTHED_SPLICED)
require_var(adata, BETA)
u_smooth = get_layer(adata, SMOOTHED_UNSPLICED).astype(np.float32)
s_smooth = get_layer(adata, SMOOTHED_SPLICED).astype(np.float32)
beta = adata.var[BETA].values.astype(np.float32)
if mode == "steady_state":
logger.info("Estimating gamma in steady-state mode.")
raw_gamma = _steady_state_gamma(u_smooth, s_smooth, beta, min_spliced)
elif mode == "dynamic":
if velocity_layer is None:
raise ValueError(
"velocity_layer must be provided when mode='dynamic'."
)
require_layers(adata, velocity_layer)
ds_dt = get_layer(adata, velocity_layer).astype(np.float32)
logger.info("Estimating gamma in dynamic mode (velocity_layer=%r).", velocity_layer)
raw_gamma = _dynamic_gamma(u_smooth, s_smooth, beta, ds_dt, min_spliced)
else:
raise ValueError(f"Unknown mode: {mode!r}. Use 'steady_state' or 'dynamic'.")
# Per-gene clip at upper quantile
clip_vals = np.quantile(raw_gamma, clip_quantile, axis=0).astype(np.float32)
clip_vals = np.maximum(clip_vals, 1e-6)
out = np.minimum(raw_gamma, clip_vals[np.newaxis, :])
# Global clip: use per-gene median distribution to set a reasonable cap.
# Cap at 10x the 99th percentile of per-gene medians — this removes
# extreme gene-level outliers while preserving meaningful variation.
gene_medians = np.median(out, axis=0)
positive_medians = gene_medians[gene_medians > 0]
if len(positive_medians) > 0:
global_cap = 10.0 * np.percentile(positive_medians, 99)
out = np.minimum(out, global_cap)
adata.layers[GAMMA] = out
log_params(adata, "estimate_gamma", {
"clip_quantile": clip_quantile,
"mode": mode,
"velocity_layer": velocity_layer,
"min_spliced": min_spliced,
})
def _steady_state_gamma(
u_smooth: np.ndarray,
s_smooth: np.ndarray,
beta: np.ndarray,
min_spliced: float = 0.01,
) -> np.ndarray:
"""Steady-state gamma: beta * u / s.
Sets gamma=0 where spliced counts are below threshold (unreliable ratio).
"""
# Mask: only compute gamma where spliced signal is meaningful
reliable = s_smooth >= min_spliced
s_safe = np.where(reliable, s_smooth, 1.0) # placeholder where unreliable
raw_gamma = beta[np.newaxis, :] * u_smooth / s_safe
raw_gamma = np.clip(raw_gamma, 0.0, None)
# Zero out unreliable entries
raw_gamma[~reliable] = 0.0
return raw_gamma.astype(np.float32)
def _dynamic_gamma(
u_smooth: np.ndarray,
s_smooth: np.ndarray,
beta: np.ndarray,
ds_dt: np.ndarray,
min_spliced: float = 0.01,
) -> np.ndarray:
"""Dynamic gamma: (beta * u - ds/dt) / s from the full ODE."""
reliable = s_smooth >= min_spliced
s_safe = np.where(reliable, s_smooth, 1.0)
raw_gamma = (beta[np.newaxis, :] * u_smooth - ds_dt) / s_safe
raw_gamma = np.clip(raw_gamma, 0.0, None)
raw_gamma[~reliable] = 0.0
return raw_gamma.astype(np.float32)