| """Numba-JIT compiled kernels for scPTR.""" |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
| import numba as nb |
|
|
|
|
| @nb.njit(parallel=True, cache=True) |
| def _smooth_kernel( |
| data: np.ndarray, |
| indices_flat: np.ndarray, |
| indptr: np.ndarray, |
| distances_flat: np.ndarray, |
| bandwidths: np.ndarray, |
| out: np.ndarray, |
| ) -> None: |
| """Gaussian kernel smoothing over kNN graph. |
| |
| Parameters |
| ---------- |
| data : (n_obs, n_genes) float32 |
| Gene expression matrix to smooth. |
| indices_flat : (nnz,) int32 |
| CSR indices array of the kNN graph. |
| indptr : (n_obs+1,) int32 |
| CSR indptr array of the kNN graph. |
| distances_flat : (nnz,) float32 |
| CSR data array (distances) of the kNN graph. |
| bandwidths : (n_obs,) float32 |
| Per-cell bandwidth for the Gaussian kernel. |
| out : (n_obs, n_genes) float32 |
| Output array (pre-allocated). |
| """ |
| n_obs = data.shape[0] |
| n_genes = data.shape[1] |
|
|
| for i in nb.prange(n_obs): |
| start = indptr[i] |
| end = indptr[i + 1] |
| bw = bandwidths[i] |
| bw2 = bw * bw |
| if bw2 < 1e-12: |
| bw2 = 1e-12 |
|
|
| |
| n_neighbors = end - start |
| weights = np.empty(n_neighbors + 1, dtype=np.float32) |
| neighbor_idx = np.empty(n_neighbors + 1, dtype=np.int64) |
|
|
| |
| weights[0] = 1.0 |
| neighbor_idx[0] = i |
| total_weight = 1.0 |
|
|
| for k in range(n_neighbors): |
| j = indices_flat[start + k] |
| d = distances_flat[start + k] |
| w = np.exp(-0.5 * d * d / bw2) |
| weights[k + 1] = w |
| neighbor_idx[k + 1] = j |
| total_weight += w |
|
|
| |
| inv_total = 1.0 / total_weight |
| for g in range(n_genes): |
| val = 0.0 |
| for k in range(n_neighbors + 1): |
| val += weights[k] * data[neighbor_idx[k], g] |
| out[i, g] = val * inv_total |
|
|
|
|
| @nb.njit(parallel=True, cache=True) |
| def _compute_adaptive_bandwidths( |
| distances_flat: np.ndarray, |
| indptr: np.ndarray, |
| ) -> np.ndarray: |
| """Compute per-cell adaptive bandwidth as median neighbor distance. |
| |
| Parameters |
| ---------- |
| distances_flat : (nnz,) float32 |
| indptr : (n_obs+1,) int32 |
| |
| Returns |
| ------- |
| bandwidths : (n_obs,) float32 |
| """ |
| n_obs = indptr.shape[0] - 1 |
| bandwidths = np.empty(n_obs, dtype=np.float32) |
|
|
| for i in nb.prange(n_obs): |
| start = indptr[i] |
| end = indptr[i + 1] |
| n_neighbors = end - start |
| if n_neighbors == 0: |
| bandwidths[i] = 1.0 |
| continue |
|
|
| |
| dists = np.empty(n_neighbors, dtype=np.float32) |
| for k in range(n_neighbors): |
| dists[k] = distances_flat[start + k] |
| dists.sort() |
|
|
| mid = n_neighbors // 2 |
| if n_neighbors % 2 == 0: |
| bandwidths[i] = (dists[mid - 1] + dists[mid]) / 2.0 |
| else: |
| bandwidths[i] = dists[mid] |
|
|
| if bandwidths[i] < 1e-6: |
| bandwidths[i] = 1.0 |
|
|
| return bandwidths |
|
|
|
|
| @nb.njit(parallel=True, cache=True) |
| def _compute_gamma_kernel( |
| u_smooth: np.ndarray, |
| s_smooth: np.ndarray, |
| beta: np.ndarray, |
| clip_vals: np.ndarray, |
| out: np.ndarray, |
| ) -> None: |
| """Compute per-cell per-gene degradation rate gamma. |
| |
| gamma[i,g] = beta[g] * u_smooth[i,g] / max(s_smooth[i,g], 1e-6) |
| Clipped at clip_vals[g] per gene. |
| |
| Parameters |
| ---------- |
| u_smooth : (n_obs, n_genes) float32 |
| s_smooth : (n_obs, n_genes) float32 |
| beta : (n_genes,) float32 |
| clip_vals : (n_genes,) float32 |
| Per-gene clip values (e.g., 99th percentile). |
| out : (n_obs, n_genes) float32 |
| """ |
| n_obs = u_smooth.shape[0] |
| n_genes = u_smooth.shape[1] |
|
|
| for i in nb.prange(n_obs): |
| for g in range(n_genes): |
| s_val = s_smooth[i, g] |
| if s_val < 1e-6: |
| s_val = 1e-6 |
| gamma_val = beta[g] * u_smooth[i, g] / s_val |
| if gamma_val > clip_vals[g]: |
| gamma_val = clip_vals[g] |
| if gamma_val < 0.0: |
| gamma_val = 0.0 |
| out[i, g] = gamma_val |
|
|
|
|
| @nb.njit(parallel=True, cache=True) |
| def _velocity_kernel( |
| gamma: np.ndarray, |
| indices_flat: np.ndarray, |
| indptr: np.ndarray, |
| distances_flat: np.ndarray, |
| bandwidths: np.ndarray, |
| out: np.ndarray, |
| ) -> None: |
| """Compute PT velocity as weighted mean gamma difference from neighbors. |
| |
| v[i,g] = sum_j(w_ij * (gamma[j,g] - gamma[i,g])) |
| |
| Parameters |
| ---------- |
| gamma : (n_obs, n_genes) float32 |
| indices_flat, indptr, distances_flat : kNN graph CSR arrays |
| bandwidths : (n_obs,) float32 |
| out : (n_obs, n_genes) float32 |
| """ |
| n_obs = gamma.shape[0] |
| n_genes = gamma.shape[1] |
|
|
| for i in nb.prange(n_obs): |
| start = indptr[i] |
| end = indptr[i + 1] |
| bw = bandwidths[i] |
| bw2 = bw * bw |
| if bw2 < 1e-12: |
| bw2 = 1e-12 |
|
|
| n_neighbors = end - start |
| if n_neighbors == 0: |
| for g in range(n_genes): |
| out[i, g] = 0.0 |
| continue |
|
|
| |
| total_weight = 0.0 |
| for k in range(n_neighbors): |
| d = distances_flat[start + k] |
| w = np.exp(-0.5 * d * d / bw2) |
| total_weight += w |
|
|
| if total_weight < 1e-12: |
| for g in range(n_genes): |
| out[i, g] = 0.0 |
| continue |
|
|
| inv_total = 1.0 / total_weight |
|
|
| for g in range(n_genes): |
| val = 0.0 |
| for k in range(n_neighbors): |
| j = indices_flat[start + k] |
| d = distances_flat[start + k] |
| w = np.exp(-0.5 * d * d / bw2) |
| val += w * (gamma[j, g] - gamma[i, g]) |
| out[i, g] = val * inv_total |
|
|