Source code for deepspatial.data_utils.uot_solver

import gc
import numpy as np
import ot

[docs] def compute_cost_matrix( x0, g0, c0, x1, g1, c1, alpha_spatial=0.5, cell_type_penalty=50.0, ): """ Computes a hybrid cost matrix balancing spatial distance, gene expression, and cell types. Parameters ---------- x0 : numpy.ndarray or torch.Tensor Spatial coordinates for the source slice, shape `(N0, 2)`. g0 : numpy.ndarray or torch.Tensor Gene expression matrix for the source slice, shape `(N0, G)`. c0 : numpy.ndarray or torch.Tensor One-hot encoded cell types for the source slice, shape `(N0, C)`. x1 : numpy.ndarray or torch.Tensor Spatial coordinates for the target slice, shape `(N1, 2)`. g1 : numpy.ndarray or torch.Tensor Gene expression matrix for the target slice, shape `(N1, G)`. c1 : numpy.ndarray or torch.Tensor One-hot encoded cell types for the target slice, shape `(N1, C)`. alpha_spatial : float, optional Weight balancing the spatial vs. gene distance (between 0 and 1). By default 0.5. cell_type_penalty : float, optional Additive cost for pairing cells with different labels. The default of 50.0 is the value used for the manuscript experiments. Returns ------- numpy.ndarray The combined cost matrix of shape `(N0, N1)`. """ eps = 1e-8 # Spatial distance (Euclidean) # x: [N0, 2], y: [N1, 2] -> M: [N0, N1] cost_spatial = ot.dist(x0, x1, metric='euclidean') cost_spatial = cost_spatial / (cost_spatial.max() + eps) + eps # Gene expression distance (Cosine) cost_gene = ot.dist(g0, g1, metric='cosine') cost_gene = cost_gene / (cost_gene.max() + eps) + eps # Cell type cost (One-hot dot product) # 1.0 - dot(c0, c1.T) gives 0 for same type, 1 for different types c0_np = c0.numpy() if hasattr(c0, 'numpy') else c0 c1_np = c1.numpy() if hasattr(c1, 'numpy') else c1 cost_class = np.clip(1.0 - np.dot(c0_np, c1_np.T), eps, 1) C = ( (alpha_spatial * cost_spatial) + ((1 - alpha_spatial) * cost_gene) + (cell_type_penalty * cost_class) ) # Clean up large intermediate distance matrices del cost_spatial, cost_gene, cost_class gc.collect() return C
[docs] def compute_uot_coupling(x0, g0, c0, x1, g1, c1, alpha_spatial=0.5, cell_type_penalty=50.0, uot_reg=0.8, uot_tau=0.05, num_itermax=50, stop_thr=1e-2): """ Computes the Unbalanced Optimal Transport (UOT) coupling matrix between two slices. Parameters ---------- x0 : numpy.ndarray or torch.Tensor Spatial coordinates for the source slice. g0 : numpy.ndarray or torch.Tensor Gene expression matrix for the source slice. c0 : numpy.ndarray or torch.Tensor One-hot encoded cell types for the source slice. x1 : numpy.ndarray or torch.Tensor Spatial coordinates for the target slice. g1 : numpy.ndarray or torch.Tensor Gene expression matrix for the target slice. c1 : numpy.ndarray or torch.Tensor One-hot encoded cell types for the target slice. alpha_spatial : float, optional Weight for spatial distance in the underlying cost matrix. By default 0.5. cell_type_penalty : float, optional Cost multiplier for cross-cell-type couplings. By default 50.0, as in the manuscript experiments. uot_reg : float, optional Entropy regularization parameter. Higher values lead to smoother, more dispersed couplings; lower values yield sparser, more deterministic matchings. By default 0.8. uot_tau : float, optional Marginal relaxation weight (KL divergence penalty). Controls how strictly the mass conservation is enforced. Smaller values allow more mass creation/destruction. By default 0.05. num_itermax : int, optional Maximum Sinkhorn iterations. By default 50. stop_thr : float, optional Sinkhorn stopping threshold. By default 1e-2. Returns ------- numpy.ndarray The calculated optimal transport coupling matrix `pi` of shape `(N0, N1)`. """ # Generate the cost matrix C = compute_cost_matrix( x0, g0, c0, x1, g1, c1, alpha_spatial=alpha_spatial, cell_type_penalty=cell_type_penalty, ) # Define marginal weights (uniform distribution) n0, n1 = x0.shape[0], x1.shape[0] a = np.ones(n0) / n0 + 1e-8 b = np.ones(n1) / n1 + 1e-8 # Solve Unbalanced Sinkhorn # reg_m (uot_tau) controls how much we allow marginals to be violated pi = ot.unbalanced.sinkhorn_unbalanced( a, b, C, reg=max(uot_reg, 0.01), reg_m=uot_tau, numItermax=num_itermax, stopThr=stop_thr, verbose=False ) # 4. Final memory cleanup del C, a, b gc.collect() return pi