mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-15 20:57:56 +08:00
Many of our matrices are log normalized, which tends to eliminate the number of non zero values (if there were any). This prevents the matrix from being stored as a sparse matrix. The solution here is to use a simple transformation to make it sparse again. The most common value from each column is subtracted from that column. These values that were subtracted are saved in an array called X_col_shift. The cellxgene code needs to understand how to undo the transformation when operating over the X matrix. - added script to create a synthetic dataset for testing - added a script to convert an existing CXG dataset to a sparse CXG dataset
199 lines
7.4 KiB
Python
199 lines
7.4 KiB
Python
import concurrent.futures
|
|
import numpy as np
|
|
from server.compute.diffexp_generic import diffexp_ttest_from_mean_var, mean_var_n
|
|
from server.data_cxg.cxg_util import pack_selector_from_indices
|
|
from server.common.errors import ComputeError
|
|
from numba import jit
|
|
|
|
|
|
"""
|
|
See the comments in diffexp_generic for a description of this algorithm
|
|
|
|
This implementation runs directly in-process. It is multi- threaded, but not particularly scalable.
|
|
Longer term, will likely move to a distributed framework for this.
|
|
|
|
There are currently no global throttles on simultaneous workers.
|
|
"""
|
|
|
|
diffexp_thread_executor = None
|
|
max_workers = None
|
|
target_workunit = None
|
|
|
|
|
|
def set_config(config_max_workers, config_target_workunit):
|
|
global max_workers
|
|
global target_workunit
|
|
max_workers = config_max_workers
|
|
target_workunit = config_target_workunit
|
|
|
|
|
|
def get_thread_executor():
|
|
global diffexp_thread_executor
|
|
if diffexp_thread_executor is None:
|
|
diffexp_thread_executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
|
|
return diffexp_thread_executor
|
|
|
|
|
|
def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
|
|
|
|
matrix = adaptor.open_array("X")
|
|
row_selector_A = np.where(maskA)[0]
|
|
row_selector_B = np.where(maskB)[0]
|
|
nA = len(row_selector_A)
|
|
nB = len(row_selector_B)
|
|
|
|
dtype = matrix.dtype
|
|
cols = matrix.shape[1]
|
|
tile_extent = [dim.tile for dim in matrix.schema.domain]
|
|
|
|
is_sparse = matrix.schema.sparse
|
|
|
|
if is_sparse:
|
|
row_selector_A = pack_selector_from_indices(row_selector_A)
|
|
row_selector_B = pack_selector_from_indices(row_selector_B)
|
|
else:
|
|
# The rows from both row_selector_A and row_selector_B are gathered at the
|
|
# same time, then the mean and variance are computed by subsetting on that
|
|
# combined submatrix. Combining the gather reduces number of requests/bandwidth
|
|
# to the data source.
|
|
row_selector_AB = np.union1d(row_selector_A, row_selector_B)
|
|
row_selector_A_in_AB = np.in1d(row_selector_AB, row_selector_A, assume_unique=True)
|
|
row_selector_B_in_AB = np.in1d(row_selector_AB, row_selector_B, assume_unique=True)
|
|
row_selector_AB = pack_selector_from_indices(row_selector_AB)
|
|
|
|
# because all IO is done per-tile, and we are always col-major,
|
|
# use the tile column size as the unit of partition. Possibly access
|
|
# more than one column tile at a time based on the target_workunit.
|
|
# Revisit partitioning if we change the X layout, or start using a non-local execution environment
|
|
# which may have other constraints.
|
|
|
|
# TODO: If the number of row selections is large enough, then the cells_per_coltile will exceed
|
|
# the target_workunit. A potential improvement would be to partition by both columns and rows.
|
|
# However partitioning the rows is slightly more complex due to the arbitrary distribution
|
|
# of row selections that are passed into this algorithm.
|
|
|
|
cells_per_coltile = (nA + nB) * tile_extent[1]
|
|
cols_per_partition = max(1, int(target_workunit / cells_per_coltile)) * tile_extent[1]
|
|
col_partitions = [(c, min(c + cols_per_partition, cols)) for c in range(0, cols, cols_per_partition)]
|
|
|
|
meanA = np.zeros((cols,), dtype=np.float64)
|
|
varA = np.zeros((cols,), dtype=np.float64)
|
|
meanB = np.zeros((cols,), dtype=np.float64)
|
|
varB = np.zeros((cols,), dtype=np.float64)
|
|
|
|
executor = get_thread_executor()
|
|
futures = []
|
|
|
|
if is_sparse:
|
|
for cols in col_partitions:
|
|
futures.append(executor.submit(_mean_var_sparse_ab, matrix, row_selector_A, nA, row_selector_B, nB, cols))
|
|
else:
|
|
for cols in col_partitions:
|
|
futures.append(
|
|
executor.submit(_mean_var_ab, matrix, row_selector_AB, row_selector_A_in_AB, row_selector_B_in_AB, cols)
|
|
)
|
|
|
|
for future in futures:
|
|
# returns tuple: (meanA, varA, meanB, varB, cols)
|
|
try:
|
|
result = future.result()
|
|
part_meanA, part_varA, part_meanB, part_varB, cols = result
|
|
meanA[cols[0] : cols[1]] += part_meanA
|
|
varA[cols[0] : cols[1]] += part_varA
|
|
meanB[cols[0] : cols[1]] += part_meanB
|
|
varB[cols[0] : cols[1]] += part_varB
|
|
except Exception as e:
|
|
for future in futures:
|
|
future.cancel()
|
|
raise ComputeError(str(e))
|
|
|
|
if is_sparse:
|
|
if adaptor.has_array("X_col_shift"):
|
|
X_col_shift = adaptor.open_array("X_col_shift")[:]
|
|
meanA += X_col_shift
|
|
meanB += X_col_shift
|
|
|
|
r = diffexp_ttest_from_mean_var(
|
|
meanA.astype(dtype),
|
|
varA.astype(dtype),
|
|
nA,
|
|
meanB.astype(dtype),
|
|
varB.astype(dtype),
|
|
nB,
|
|
top_n,
|
|
diffexp_lfc_cutoff,
|
|
)
|
|
|
|
return r
|
|
|
|
|
|
def _mean_var_ab(matrix, row_selector_AB, row_selector_A_in_AB, row_selector_B_in_AB, col_range):
|
|
X = matrix.multi_index[row_selector_AB, col_range[0] : col_range[1] - 1][""]
|
|
meanA, varA, n = mean_var_n(X[row_selector_A_in_AB])
|
|
meanB, varB, n = mean_var_n(X[row_selector_B_in_AB])
|
|
return (meanA, varA, meanB, varB, col_range)
|
|
|
|
|
|
def _mean_var_sparse_ab(matrix, row_selector_A, nrows_A, row_selector_B, nrows_B, col_range):
|
|
meanA, varA = _mean_var_sparse(matrix, row_selector_A, nrows_A, col_range)
|
|
meanB, varB = _mean_var_sparse(matrix, row_selector_B, nrows_B, col_range)
|
|
return (meanA, varA, meanB, varB, col_range)
|
|
|
|
|
|
@jit(nopython=True)
|
|
def _mean_var_sparse_numba(x, var, nrows, ncols):
|
|
"""Kernel to compute the mean and variance. It was not clear if this function
|
|
could be written using numpy, thus avoiding the loops. Therefore numba is
|
|
used here to speed things up. With numba, this function takes a negligible amount
|
|
of time compared to reading in the sparse matrix"""
|
|
mean = np.zeros((ncols,), dtype=np.float64)
|
|
for col, val in zip(var, x):
|
|
mean[col] += val
|
|
mean /= nrows
|
|
|
|
# optimize the sumsq computation.
|
|
# since most entries in a sparse matrix are 0, then start by assuming
|
|
# all values are 0, so fill the sumsq array with nrows * (0 - mean)**2.
|
|
# as non-zero values are encountered, subtract off the (mean*mean) value
|
|
# and replace with (val-mean)**2. Simplifying the expression
|
|
# gives the following code.
|
|
sumsq = nrows * np.multiply(mean, mean)
|
|
for col, val in zip(var, x):
|
|
sumsq[col] += val * (val - 2 * mean[col])
|
|
v = sumsq / (nrows - 1)
|
|
return mean, v
|
|
|
|
|
|
def _mean_var_sparse(matrix, selector, nrows, col_range):
|
|
data = matrix.multi_index[selector, col_range[0] : col_range[1] - 1]
|
|
x = data[""]
|
|
|
|
# tiledb < 0.6.0 and >= 0.6.0 have slightly different interfaces.
|
|
# the following takes care of both cases:
|
|
# older: data["coords]["var"]
|
|
# newer: data["var"]
|
|
var = data.get("coords", data)["var"]
|
|
|
|
# shift the column indices to start at 0, this
|
|
# will become the index into the mean and var arrays.
|
|
var -= col_range[0]
|
|
|
|
fp_err_occurred = False
|
|
|
|
def fp_err_set(err, flag):
|
|
nonlocal fp_err_occurred
|
|
fp_err_occurred = True
|
|
|
|
ncols = col_range[1] - col_range[0]
|
|
with np.errstate(divide="call", invalid="call", call=fp_err_set):
|
|
mean, v = _mean_var_sparse_numba(x, var, nrows, ncols)
|
|
|
|
if fp_err_occurred:
|
|
mean[np.isfinite(mean) == False] = 0 # noqa: E712
|
|
v[np.isfinite(v) == False] = 0 # noqa: E712
|
|
else:
|
|
mean[np.isnan(mean)] = 0
|
|
v[np.isnan(v)] = 0
|
|
|
|
return mean, v
|