| """Generate synthetic scRNA-seq data from known kinetic parameters + NB noise.""" |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
| from anndata import AnnData |
|
|
|
|
| def generate_kinetic_data( |
| n_cells: int = 3000, |
| n_genes: int = 200, |
| n_cell_types: int = 5, |
| dispersion: float = 10.0, |
| sparsity: float = 0.3, |
| seed: int = 0, |
| ) -> tuple[AnnData, dict[str, np.ndarray]]: |
| """Generate (u, s) counts from a kinetic model with NB observation noise. |
| |
| The generative process: |
| 1. Sample latent factors z_T, z_PT per cell (different means per type). |
| 2. Derive alpha = softplus(W_alpha @ z_T), gamma = softplus(W_gamma @ z_PT). |
| 3. Set beta as gene-specific constants. |
| 4. Compute mu_u ∝ alpha/beta, mu_s ∝ alpha/gamma, scaled by library size. |
| 5. Draw counts from NB(mu, theta=dispersion). |
| 6. Apply zero-inflation (dropout) at rate ``sparsity``. |
| |
| Parameters |
| ---------- |
| n_cells |
| Number of cells. |
| n_genes |
| Number of genes. |
| n_cell_types |
| Number of simulated cell types. |
| dispersion |
| NB inverse dispersion (higher = less noise). |
| sparsity |
| Fraction of zeros injected (dropout). |
| seed |
| Random seed. |
| |
| Returns |
| ------- |
| adata : AnnData |
| With layers ``'spliced'``, ``'unspliced'``, and obs ``'cell_type'``. |
| truth : dict |
| Ground-truth arrays: ``alpha``, ``gamma``, ``beta``, ``z_T``, ``z_PT``. |
| """ |
| rng = np.random.RandomState(seed) |
|
|
| d_latent = 10 |
|
|
| |
| cell_types = rng.choice(n_cell_types, size=n_cells) |
|
|
| |
| type_means_T = rng.randn(n_cell_types, d_latent).astype(np.float32) |
| type_means_PT = rng.randn(n_cell_types, d_latent).astype(np.float32) |
|
|
| z_T = type_means_T[cell_types] + 0.3 * rng.randn(n_cells, d_latent).astype( |
| np.float32 |
| ) |
| z_PT = type_means_PT[cell_types] + 0.3 * rng.randn(n_cells, d_latent).astype( |
| np.float32 |
| ) |
|
|
| |
| W_alpha = rng.randn(d_latent, n_genes).astype(np.float32) * 0.5 |
| W_gamma = rng.randn(d_latent, n_genes).astype(np.float32) * 0.5 |
|
|
| |
| alpha = _softplus(z_T @ W_alpha) |
| gamma = _softplus(z_PT @ W_gamma) |
| beta = np.exp(rng.randn(n_genes).astype(np.float32) * 0.5 + 1.0) |
|
|
| |
| eps = 1e-8 |
| mu_u_raw = alpha / (beta[np.newaxis, :] + eps) |
| mu_s_raw = alpha / (gamma + eps) |
|
|
| |
| l_u = rng.lognormal(mean=8.0, sigma=0.5, size=n_cells).astype(np.float32) |
| l_s = rng.lognormal(mean=9.0, sigma=0.5, size=n_cells).astype(np.float32) |
|
|
| |
| mu_u = (mu_u_raw / (mu_u_raw.sum(axis=1, keepdims=True) + eps)) * l_u[:, None] |
| mu_s = (mu_s_raw / (mu_s_raw.sum(axis=1, keepdims=True) + eps)) * l_s[:, None] |
|
|
| |
| u_counts = _sample_nb(mu_u, dispersion, rng) |
| s_counts = _sample_nb(mu_s, dispersion, rng) |
|
|
| |
| if sparsity > 0: |
| mask_u = rng.rand(n_cells, n_genes) > sparsity |
| mask_s = rng.rand(n_cells, n_genes) > sparsity |
| u_counts = u_counts * mask_u |
| s_counts = s_counts * mask_s |
|
|
| adata = AnnData( |
| X=s_counts.astype(np.float32), |
| layers={ |
| "spliced": s_counts.astype(np.float32), |
| "unspliced": u_counts.astype(np.float32), |
| }, |
| ) |
| adata.obs_names = [f"cell_{i}" for i in range(n_cells)] |
| adata.var_names = [f"gene_{i}" for i in range(n_genes)] |
| adata.obs["cell_type"] = [f"type_{t}" for t in cell_types] |
| adata.obs["cell_type"] = adata.obs["cell_type"].astype("category") |
|
|
| truth = { |
| "alpha": alpha.astype(np.float32), |
| "gamma": gamma.astype(np.float32), |
| "beta": beta.astype(np.float32), |
| "z_T": z_T, |
| "z_PT": z_PT, |
| } |
|
|
| return adata, truth |
|
|
|
|
| def _softplus(x: np.ndarray) -> np.ndarray: |
| """Numerically stable softplus.""" |
| return np.where(x > 20, x, np.log1p(np.exp(np.clip(x, -20, 20)))) |
|
|
|
|
| def _sample_nb( |
| mu: np.ndarray, theta: float, rng: np.random.RandomState |
| ) -> np.ndarray: |
| """Sample from NB(mu, theta) using gamma-Poisson mixture.""" |
| mu = np.clip(mu, 1e-8, None) |
| |
| p = theta / (theta + mu) |
| counts = rng.negative_binomial(theta, p) |
| return counts.astype(np.float32) |
|
|