| """Bulk deconvolution via constrained least squares.""" | |
| import numpy as np | |
| from scipy.optimize import nnls | |
| import logging | |
| log = logging.getLogger(__name__) | |
| def nnls_deconv(bulk: np.ndarray, signature: np.ndarray) -> np.ndarray: | |
| """Deconvolve bulk expression into cell type proportions via NNLS. | |
| Solves: bulk_i ≈ signature @ p_i, s.t. p_i >= 0, then normalizes to simplex. | |
| Args: | |
| bulk: (n_samples, n_genes) | |
| signature: (n_genes, K) | |
| Returns: | |
| proportions_hat: (n_samples, K), rows sum to 1 | |
| """ | |
| n = bulk.shape[0] | |
| K = signature.shape[1] | |
| props = np.zeros((n, K)) | |
| for i in range(n): | |
| coef, _ = nnls(signature, bulk[i]) | |
| total = coef.sum() | |
| if total > 0: | |
| props[i] = coef / total | |
| else: | |
| props[i] = 1.0 / K # fallback to uniform | |
| log.info(f"Deconvolved {n} samples into {K} types") | |
| return props | |