mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-05 08:58:11 +08:00
feat: diffexp returns two genesets (#2230)
* feat: return two lists for diffexp (#2221) * sp * split out derive sort order, tests passing * sp * return diff exp results in two lists * update * copy implementation over to desktop * add tests for two lists * small fixes to complete backend implementation * accept new diffexp response * map diff exp response to genesets * delete ) * name diffexp genesets with population names * take constants out of state and allow width prop to override * shorten mini-histo properly truncate and resize depending on expansion * prepend new genesets * rename data within diffexp action * backend * move diffexp ttest to common code module, update tests * update for unit tests * reference actual var Co-authored-by: Madison Dunitz <madison.dunitz@chanzuckerberg.com> Co-authored-by: Madison Dunitz <dunitzm@gmail.com>
This commit is contained in:
co-authored by
Madison Dunitz
Madison Dunitz
parent
7ed53c0f5b
commit
28b526b3fc
+17
-9
@@ -25,7 +25,8 @@ def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
|
|||||||
:param maskB: observation selection mask for set 2
|
:param maskB: observation selection mask for set 2
|
||||||
:param top_n: number of variables to return stats for
|
:param top_n: number of variables to return stats for
|
||||||
:param diffexp_lfc_cutoff: minimum
|
:param diffexp_lfc_cutoff: minimum
|
||||||
:return: for top N genes, [ varindex, logfoldchange, pval, pval_adj ]
|
absolute value returning [ varindex, logfoldchange, pval, pval_adj ] for top N genes
|
||||||
|
:return: for top N genes, {"positive": for top N genes, [ varindex, logfoldchange, pval, pval_adj ], "negative": for top N genes, [ varindex, logfoldchange, pval, pval_adj ]}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dataA = adaptor.get_X_array(maskA, None)
|
dataA = adaptor.get_X_array(maskA, None)
|
||||||
@@ -66,24 +67,27 @@ def diffexp_ttest_from_mean_var(meanA, varA, nA, meanB, varB, nB, top_n, diffexp
|
|||||||
# logfoldchanges: log2(meanA / meanB)
|
# logfoldchanges: log2(meanA / meanB)
|
||||||
logfoldchanges = np.log2(np.abs((meanA + 1e-9) / (meanB + 1e-9)))
|
logfoldchanges = np.log2(np.abs((meanA + 1e-9) / (meanB + 1e-9)))
|
||||||
|
|
||||||
|
stats_to_sort = tscores
|
||||||
# find all with lfc > cutoff
|
# find all with lfc > cutoff
|
||||||
lfc_above_cutoff_idx = np.nonzero(np.abs(logfoldchanges) > diffexp_lfc_cutoff)[0]
|
lfc_above_cutoff_idx = np.nonzero(np.abs(logfoldchanges) > diffexp_lfc_cutoff)[0]
|
||||||
stats_to_sort = np.abs(tscores)
|
|
||||||
|
|
||||||
# derive sort order
|
# derive sort order
|
||||||
if lfc_above_cutoff_idx.shape[0] > top_n:
|
if lfc_above_cutoff_idx.shape[0] > top_n*2:
|
||||||
# partition top N
|
# partition top N
|
||||||
rel_t_partition = np.argpartition(stats_to_sort[lfc_above_cutoff_idx], -top_n)[-top_n:]
|
rel_t_partition = np.argpartition(stats_to_sort[lfc_above_cutoff_idx], (top_n, -top_n))
|
||||||
t_partition = lfc_above_cutoff_idx[rel_t_partition]
|
rel_t_partition_top_n = np.concatenate((rel_t_partition[-top_n:], rel_t_partition[:top_n]))
|
||||||
|
t_partition = lfc_above_cutoff_idx[rel_t_partition_top_n]
|
||||||
# sort the top N partition
|
# sort the top N partition
|
||||||
rel_sort_order = np.argsort(stats_to_sort[t_partition])[::-1]
|
rel_sort_order = np.argsort(stats_to_sort[t_partition])[::-1]
|
||||||
sort_order = t_partition[rel_sort_order]
|
sort_order = t_partition[rel_sort_order]
|
||||||
else:
|
else:
|
||||||
# partition and sort top N, ignoring lfc cutoff
|
# partition and sort top N, ignoring lfc cutoff
|
||||||
partition = np.argpartition(stats_to_sort, -top_n)[-top_n:]
|
partition = np.argpartition(stats_to_sort, (top_n, -top_n))
|
||||||
rel_sort_order = np.argsort(stats_to_sort[partition])[::-1]
|
partition_top_n = np.concatenate((partition[-top_n:], partition[:top_n]))
|
||||||
|
|
||||||
|
rel_sort_order = np.argsort(stats_to_sort[partition_top_n])[::-1]
|
||||||
indices = np.indices(stats_to_sort.shape)[0]
|
indices = np.indices(stats_to_sort.shape)[0]
|
||||||
sort_order = indices[partition][rel_sort_order]
|
sort_order = indices[partition_top_n][rel_sort_order]
|
||||||
|
|
||||||
# top n slice based upon sort order
|
# top n slice based upon sort order
|
||||||
logfoldchanges_top_n = logfoldchanges[sort_order]
|
logfoldchanges_top_n = logfoldchanges[sort_order]
|
||||||
@@ -91,7 +95,11 @@ def diffexp_ttest_from_mean_var(meanA, varA, nA, meanB, varB, nB, top_n, diffexp
|
|||||||
pvals_adj_top_n = pvals_adj[sort_order]
|
pvals_adj_top_n = pvals_adj[sort_order]
|
||||||
|
|
||||||
# varIndex, logfoldchange, pval, pval_adj
|
# varIndex, logfoldchange, pval, pval_adj
|
||||||
result = [[sort_order[i], logfoldchanges_top_n[i], pvals_top_n[i], pvals_adj_top_n[i]] for i in range(top_n)]
|
result = {"positive": [[sort_order[i], logfoldchanges_top_n[i], pvals_top_n[i], pvals_adj_top_n[i]] for i in
|
||||||
|
range(top_n)],
|
||||||
|
"negative": [[sort_order[i], logfoldchanges_top_n[i], pvals_top_n[i], pvals_adj_top_n[i]] for i in
|
||||||
|
range(-1, -1 - top_n, -1)], }
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -260,7 +260,6 @@ def diffexp_obs_post(request, data_adaptor):
|
|||||||
try:
|
try:
|
||||||
# TODO: implement varfilter mode
|
# TODO: implement varfilter mode
|
||||||
mode = DiffExpMode(args["mode"])
|
mode = DiffExpMode(args["mode"])
|
||||||
|
|
||||||
if mode == DiffExpMode.VAR_FILTER or "varFilter" in args:
|
if mode == DiffExpMode.VAR_FILTER or "varFilter" in args:
|
||||||
return abort_and_log(HTTPStatus.NOT_IMPLEMENTED, "varFilter not enabled")
|
return abort_and_log(HTTPStatus.NOT_IMPLEMENTED, "varFilter not enabled")
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import numpy as np
|
|||||||
from numba import jit
|
from numba import jit
|
||||||
|
|
||||||
from backend.czi_hosted.data_cxg.cxg_util import pack_selector_from_indices
|
from backend.czi_hosted.data_cxg.cxg_util import pack_selector_from_indices
|
||||||
from backend.czi_hosted.compute.diffexp_generic import diffexp_ttest_from_mean_var, mean_var_n
|
from backend.common.compute.diffexp_generic import diffexp_ttest_from_mean_var, mean_var_n
|
||||||
from backend.common.errors import ComputeError
|
from backend.common.errors import ComputeError
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -115,14 +115,14 @@ def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
|
|||||||
meanB += X_col_shift
|
meanB += X_col_shift
|
||||||
|
|
||||||
r = diffexp_ttest_from_mean_var(
|
r = diffexp_ttest_from_mean_var(
|
||||||
meanA.astype(dtype),
|
meanA=meanA.astype(dtype),
|
||||||
varA.astype(dtype),
|
varA=varA.astype(dtype),
|
||||||
nA,
|
nA=nA,
|
||||||
meanB.astype(dtype),
|
meanB=meanB.astype(dtype),
|
||||||
varB.astype(dtype),
|
varB=varB.astype(dtype),
|
||||||
nB,
|
nB=nB,
|
||||||
top_n,
|
top_n=top_n,
|
||||||
diffexp_lfc_cutoff,
|
diffexp_lfc_cutoff=diffexp_lfc_cutoff
|
||||||
)
|
)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pandas.core.dtypes.dtypes import CategoricalDtype
|
|||||||
from scipy import sparse
|
from scipy import sparse
|
||||||
from server_timing import Timing as ServerTiming
|
from server_timing import Timing as ServerTiming
|
||||||
|
|
||||||
import backend.czi_hosted.compute.diffexp_generic as diffexp_generic
|
import backend.common.compute.diffexp_generic as diffexp_generic
|
||||||
from backend.common.colors import convert_anndata_category_colors_to_cxg_category_colors
|
from backend.common.colors import convert_anndata_category_colors_to_cxg_category_colors
|
||||||
from backend.common.constants import Axis, MAX_LAYOUTS
|
from backend.common.constants import Axis, MAX_LAYOUTS
|
||||||
from backend.czi_hosted.common.corpora import corpora_get_props_from_anndata
|
from backend.czi_hosted.common.corpora import corpora_get_props_from_anndata
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
mask = np.zeros((count,), dtype=np.bool)
|
mask = np.zeros((count,), dtype=np.bool)
|
||||||
for i in filter:
|
for i in filter:
|
||||||
if type(i) == list:
|
if type(i) == list:
|
||||||
mask[i[0] : i[1]] = True
|
mask[i[0]: i[1]] = True
|
||||||
else:
|
else:
|
||||||
mask[i] = True
|
mask[i] = True
|
||||||
return mask
|
return mask
|
||||||
@@ -321,11 +321,12 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
top_n = self.dataset_config.diffexp__top_n
|
top_n = self.dataset_config.diffexp__top_n
|
||||||
|
|
||||||
if self.server_config.exceeds_limit(
|
if self.server_config.exceeds_limit(
|
||||||
"diffexp_cellcount_max", np.count_nonzero(obs_mask_A) + np.count_nonzero(obs_mask_B)
|
"diffexp_cellcount_max", np.count_nonzero(obs_mask_A) + np.count_nonzero(obs_mask_B)
|
||||||
):
|
):
|
||||||
raise ExceedsLimitError("Diffexp request exceeds max cell count limit")
|
raise ExceedsLimitError("Diffexp request exceeds max cell count limit")
|
||||||
|
|
||||||
result = self.compute_diffexp_ttest(obs_mask_A, obs_mask_B, top_n, self.dataset_config.diffexp__lfc_cutoff)
|
result = self.compute_diffexp_ttest(
|
||||||
|
maskA=obs_mask_A, maskB=obs_mask_B, top_n=top_n, lfc_cutoff=self.dataset_config.diffexp__lfc_cutoff)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return jsonify_numpy(result)
|
return jsonify_numpy(result)
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ class CxgAdaptor(DataAdaptor):
|
|||||||
top_n = self.dataset_config.diffexp__top_n
|
top_n = self.dataset_config.diffexp__top_n
|
||||||
if lfc_cutoff is None:
|
if lfc_cutoff is None:
|
||||||
lfc_cutoff = self.dataset_config.diffexp__lfc_cutoff
|
lfc_cutoff = self.dataset_config.diffexp__lfc_cutoff
|
||||||
return diffexp_cxg.diffexp_ttest(self, maskA, maskB, top_n, lfc_cutoff)
|
return diffexp_cxg.diffexp_ttest(
|
||||||
|
adaptor=self, maskA=maskA, maskB=maskB, top_n=top_n, diffexp_lfc_cutoff=lfc_cutoff)
|
||||||
|
|
||||||
def get_colors(self):
|
def get_colors(self):
|
||||||
if self.cxg_version == "0.0":
|
if self.cxg_version == "0.0":
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
from scipy import sparse, stats
|
|
||||||
|
|
||||||
|
|
||||||
def diffexp_ttest(adaptor, maskA, maskB, top_n=8, diffexp_lfc_cutoff=0.01):
|
|
||||||
"""
|
|
||||||
Return differential expression statistics for top N variables.
|
|
||||||
|
|
||||||
Algorithm:
|
|
||||||
- compute log fold change (log2(meanA/meanB))
|
|
||||||
- compute Welch's t-test statistic and pvalue (w/ Bonferroni correction)
|
|
||||||
- return top N abs(logfoldchange) where lfc > diffexp_lfc_cutoff
|
|
||||||
|
|
||||||
If there are not N which meet criteria, augment by removing the logfoldchange
|
|
||||||
threshold requirement.
|
|
||||||
|
|
||||||
Notes on alogrithm:
|
|
||||||
- Welch's ttest provides basic statistics test.
|
|
||||||
https://en.wikipedia.org/wiki/Welch%27s_t-test
|
|
||||||
- p-values adjusted with Bonferroni correction.
|
|
||||||
https://en.wikipedia.org/wiki/Bonferroni_correction
|
|
||||||
|
|
||||||
:param adaptor: DataAdaptor instance
|
|
||||||
:param maskA: observation selection mask for set 1
|
|
||||||
:param maskB: observation selection mask for set 2
|
|
||||||
:param top_n: number of variables to return stats for
|
|
||||||
:param diffexp_lfc_cutoff: minimum
|
|
||||||
:return: for top N genes, [ varindex, logfoldchange, pval, pval_adj ]
|
|
||||||
"""
|
|
||||||
|
|
||||||
dataA = adaptor.get_X_array(maskA, None)
|
|
||||||
dataB = adaptor.get_X_array(maskB, None)
|
|
||||||
|
|
||||||
# mean, variance, N - calculate for both selections
|
|
||||||
meanA, vA, nA = mean_var_n(dataA)
|
|
||||||
meanB, vB, nB = mean_var_n(dataB)
|
|
||||||
res = diffexp_ttest_from_mean_var(meanA, vA, nA, meanB, vB, nB, top_n, diffexp_lfc_cutoff)
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def diffexp_ttest_from_mean_var(meanA, varA, nA, meanB, varB, nB, top_n, diffexp_lfc_cutoff):
|
|
||||||
n_var = meanA.shape[0]
|
|
||||||
top_n = min(top_n, n_var)
|
|
||||||
|
|
||||||
# variance / N
|
|
||||||
vnA = varA / min(nA, nB) # overestimate variance, would normally be nA
|
|
||||||
vnB = varB / min(nA, nB) # overestimate variance, would normally be nB
|
|
||||||
sum_vn = vnA + vnB
|
|
||||||
|
|
||||||
# degrees of freedom for Welch's t-test
|
|
||||||
with np.errstate(divide="ignore", invalid="ignore"):
|
|
||||||
dof = sum_vn ** 2 / (vnA ** 2 / (nA - 1) + vnB ** 2 / (nB - 1))
|
|
||||||
dof[np.isnan(dof)] = 1
|
|
||||||
|
|
||||||
# Welch's t-test score calculation
|
|
||||||
with np.errstate(divide="ignore", invalid="ignore"):
|
|
||||||
tscores = (meanA - meanB) / np.sqrt(sum_vn)
|
|
||||||
tscores[np.isnan(tscores)] = 0
|
|
||||||
|
|
||||||
# p-value
|
|
||||||
pvals = stats.t.sf(np.abs(tscores), dof) * 2
|
|
||||||
pvals_adj = pvals * n_var
|
|
||||||
pvals_adj[pvals_adj > 1] = 1 # cap adjusted p-value at 1
|
|
||||||
|
|
||||||
# logfoldchanges: log2(meanA / meanB)
|
|
||||||
logfoldchanges = np.log2(np.abs((meanA + 1e-9) / (meanB + 1e-9)))
|
|
||||||
|
|
||||||
# find all with lfc > cutoff
|
|
||||||
lfc_above_cutoff_idx = np.nonzero(np.abs(logfoldchanges) > diffexp_lfc_cutoff)[0]
|
|
||||||
stats_to_sort = np.abs(tscores)
|
|
||||||
|
|
||||||
# derive sort order
|
|
||||||
if lfc_above_cutoff_idx.shape[0] > top_n:
|
|
||||||
# partition top N
|
|
||||||
rel_t_partition = np.argpartition(stats_to_sort[lfc_above_cutoff_idx], -top_n)[-top_n:]
|
|
||||||
t_partition = lfc_above_cutoff_idx[rel_t_partition]
|
|
||||||
# sort the top N partition
|
|
||||||
rel_sort_order = np.argsort(stats_to_sort[t_partition])[::-1]
|
|
||||||
sort_order = t_partition[rel_sort_order]
|
|
||||||
else:
|
|
||||||
# partition and sort top N, ignoring lfc cutoff
|
|
||||||
partition = np.argpartition(stats_to_sort, -top_n)[-top_n:]
|
|
||||||
rel_sort_order = np.argsort(stats_to_sort[partition])[::-1]
|
|
||||||
indices = np.indices(stats_to_sort.shape)[0]
|
|
||||||
sort_order = indices[partition][rel_sort_order]
|
|
||||||
|
|
||||||
# top n slice based upon sort order
|
|
||||||
logfoldchanges_top_n = logfoldchanges[sort_order]
|
|
||||||
pvals_top_n = pvals[sort_order]
|
|
||||||
pvals_adj_top_n = pvals_adj[sort_order]
|
|
||||||
|
|
||||||
# varIndex, logfoldchange, pval, pval_adj
|
|
||||||
result = [[sort_order[i], logfoldchanges_top_n[i], pvals_top_n[i], pvals_adj_top_n[i]] for i in range(top_n)]
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# Convenience function which handles sparse data
|
|
||||||
def mean_var_n(X):
|
|
||||||
"""
|
|
||||||
Two-pass variance calculation. Numerically (more) stable
|
|
||||||
than naive methods (and same method used by numpy.var())
|
|
||||||
https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Two-pass
|
|
||||||
"""
|
|
||||||
# fp_err_occurred is a flag indicating that a floating point error
|
|
||||||
# occured somewhere in our compute. Used to trigger non-finite
|
|
||||||
# number handling.
|
|
||||||
fp_err_occurred = False
|
|
||||||
|
|
||||||
def fp_err_set(err, flag):
|
|
||||||
nonlocal fp_err_occurred
|
|
||||||
fp_err_occurred = True
|
|
||||||
|
|
||||||
with np.errstate(divide="call", invalid="call", call=fp_err_set):
|
|
||||||
n = X.shape[0]
|
|
||||||
if sparse.issparse(X):
|
|
||||||
mean = X.mean(axis=0).A1
|
|
||||||
dfm = X - mean
|
|
||||||
sumsq = np.sum(np.multiply(dfm, dfm), axis=0).A1
|
|
||||||
v = sumsq / (n - 1)
|
|
||||||
else:
|
|
||||||
mean = X.mean(axis=0)
|
|
||||||
dfm = X - mean
|
|
||||||
sumsq = np.sum(np.multiply(dfm, dfm), axis=0)
|
|
||||||
v = sumsq / (n - 1)
|
|
||||||
|
|
||||||
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, n
|
|
||||||
@@ -8,7 +8,7 @@ from pandas.core.dtypes.dtypes import CategoricalDtype
|
|||||||
from scipy import sparse
|
from scipy import sparse
|
||||||
from server_timing import Timing as ServerTiming
|
from server_timing import Timing as ServerTiming
|
||||||
|
|
||||||
import backend.server.compute.diffexp_generic as diffexp_generic
|
import backend.common.compute.diffexp_generic as diffexp_generic
|
||||||
from backend.common.colors import convert_anndata_category_colors_to_cxg_category_colors
|
from backend.common.colors import convert_anndata_category_colors_to_cxg_category_colors
|
||||||
from backend.common.constants import Axis, MAX_LAYOUTS
|
from backend.common.constants import Axis, MAX_LAYOUTS
|
||||||
from backend.server.common.corpora import corpora_get_props_from_anndata
|
from backend.server.common.corpora import corpora_get_props_from_anndata
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def compute_embedding(self, method, filter):
|
def compute_embedding(self, method, filter):
|
||||||
"""compute a new embedding on the specified obs subset, and return the embedding schema. """
|
"""compute a new embedding on the specified obs subset, and return the embedding schema."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -324,7 +324,12 @@ class DataAdaptor(metaclass=ABCMeta):
|
|||||||
):
|
):
|
||||||
raise ExceedsLimitError("Diffexp request exceeds max cell count limit")
|
raise ExceedsLimitError("Diffexp request exceeds max cell count limit")
|
||||||
|
|
||||||
result = self.compute_diffexp_ttest(obs_mask_A, obs_mask_B, top_n, self.dataset_config.diffexp__lfc_cutoff)
|
result = self.compute_diffexp_ttest(
|
||||||
|
maskA=obs_mask_A,
|
||||||
|
maskB=obs_mask_B,
|
||||||
|
top_n=top_n,
|
||||||
|
lfc_cutoff=self.dataset_config.diffexp__lfc_cutoff,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return jsonify_numpy(result)
|
return jsonify_numpy(result)
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import time
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from backend.czi_hosted.common.config.app_config import AppConfig
|
from backend.czi_hosted.common.config.app_config import AppConfig
|
||||||
from backend.czi_hosted.compute import diffexp_generic, diffexp_cxg
|
from backend.czi_hosted.compute import diffexp_cxg
|
||||||
|
from backend.common.compute import diffexp_generic
|
||||||
from backend.czi_hosted.data_common.matrix_loader import MatrixDataLoader
|
from backend.czi_hosted.data_common.matrix_loader import MatrixDataLoader
|
||||||
from backend.czi_hosted.data_cxg.cxg_adaptor import CxgAdaptor
|
from backend.czi_hosted.data_cxg.cxg_adaptor import CxgAdaptor
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ class EndPoints(object):
|
|||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
self.assertEqual(result.headers["Content-Type"], "application/json")
|
||||||
result_data = result.json()
|
result_data = result.json()
|
||||||
self.assertEqual(len(result_data), 7)
|
self.assertEqual(len(result_data['positive']), 7)
|
||||||
|
self.assertEqual(len(result_data['negative']), 7)
|
||||||
|
|
||||||
def test_diff_exp_indices(self):
|
def test_diff_exp_indices(self):
|
||||||
endpoint = "diffexp/obs"
|
endpoint = "diffexp/obs"
|
||||||
@@ -173,7 +174,8 @@ class EndPoints(object):
|
|||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
self.assertEqual(result.headers["Content-Type"], "application/json")
|
||||||
result_data = result.json()
|
result_data = result.json()
|
||||||
self.assertEqual(len(result_data), 10)
|
self.assertEqual(len(result_data['positive']), 10)
|
||||||
|
self.assertEqual(len(result_data['negative']), 10)
|
||||||
|
|
||||||
def test_get_annotations_var_fbs(self):
|
def test_get_annotations_var_fbs(self):
|
||||||
endpoint = "annotations/var"
|
endpoint = "annotations/var"
|
||||||
@@ -382,6 +384,7 @@ class EndPoints(object):
|
|||||||
query_hash = hashlib.sha1(query.encode()).hexdigest()
|
query_hash = hashlib.sha1(query.encode()).hexdigest()
|
||||||
url = f"{self.URL_BASE}{endpoint}?key={query_hash}"
|
url = f"{self.URL_BASE}{endpoint}?key={query_hash}"
|
||||||
result = self.session.post(url, headers=headers, data=query)
|
result = self.session.post(url, headers=headers, data=query)
|
||||||
|
|
||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
df = decode_fbs.decode_matrix_FBS(result.content)
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import unittest
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from backend.czi_hosted.compute import diffexp_generic, diffexp_cxg
|
from backend.czi_hosted.compute import diffexp_cxg
|
||||||
|
from backend.common.compute import diffexp_generic
|
||||||
from backend.czi_hosted.compute.diffexp_cxg import diffexp_ttest
|
from backend.czi_hosted.compute.diffexp_cxg import diffexp_ttest
|
||||||
from backend.czi_hosted.converters.h5ad_data_file import H5ADDataFile
|
from backend.czi_hosted.converters.h5ad_data_file import H5ADDataFile
|
||||||
from backend.common.fbs.matrix import encode_matrix_fbs, decode_matrix_fbs
|
from backend.common.fbs.matrix import encode_matrix_fbs, decode_matrix_fbs
|
||||||
@@ -40,21 +41,37 @@ class DiffExpTest(unittest.TestCase):
|
|||||||
self.assertTrue(np.isclose(result[2], expect[2], 1e-6, 1e-4))
|
self.assertTrue(np.isclose(result[2], expect[2], 1e-6, 1e-4))
|
||||||
self.assertTrue(np.isclose(result[3], expect[3], 1e-6, 1e-4))
|
self.assertTrue(np.isclose(result[3], expect[3], 1e-6, 1e-4))
|
||||||
|
|
||||||
|
|
||||||
def check_1_10_2_10(self, results):
|
def check_1_10_2_10(self, results):
|
||||||
"""Checks the results for a specific set of rows selections"""
|
"""Checks the results for a specific set of rows selections"""
|
||||||
expects = [
|
|
||||||
|
positive_expects = [
|
||||||
|
[1712, -0.5525154, 0.0051788902660723345, 1.0],
|
||||||
|
[1575, 1.0317602, 0.007830310753043345, 1.0],
|
||||||
|
[693, 0.4703904, 0.008715846769131548, 1.0],
|
||||||
|
[916, 0.9567287, 0.009080596532247588, 1.0],
|
||||||
|
[77, 0.02665649, 0.010070392939027756, 1.0],
|
||||||
|
[782, -1.0981874, 0.010161745218916036, 1.0],
|
||||||
|
[913, 0.5683986, 0.010782030711612685, 1.0],
|
||||||
|
[910, 0.83164597, 0.014596411069229197, 1.0],
|
||||||
|
[1727, 0.4127781, 0.015168372104237176, 1.0],
|
||||||
|
[1443, -0.8241895, 0.015337080567465522, 1.0]
|
||||||
|
]
|
||||||
|
negative_expects = [
|
||||||
[956, 0.016060986, 0.0008649321884808977, 1.0],
|
[956, 0.016060986, 0.0008649321884808977, 1.0],
|
||||||
[1124, 0.96602094, 0.0011717216548271284, 1.0],
|
[1124, 0.96602094, 0.0011717216548271284, 1.0],
|
||||||
[1809, 1.1110606, 0.0019304405196777848, 1.0],
|
[1809, 1.1110606, 0.0019304405196777848, 1.0],
|
||||||
[1712, -0.5525154, 0.0051788902660723345, 1.0],
|
|
||||||
[1754, 0.5201581, 0.005691734062127954, 1.0],
|
[1754, 0.5201581, 0.005691734062127954, 1.0],
|
||||||
[948, 1.6390722, 0.006622111055981219, 1.0],
|
[948, 1.6390722, 0.006622111055981219, 1.0],
|
||||||
[1810, 0.78618884, 0.007055917428377063, 1.0],
|
[1810, 0.78618884, 0.007055917428377063, 1.0],
|
||||||
[779, 1.5241305, 0.007202934422407284, 1.0],
|
[779, 1.5241305, 0.007202934422407284, 1.0],
|
||||||
[1575, 1.0317602, 0.007830310753043345, 1.0],
|
|
||||||
[576, 0.97873515, 0.008272092578813124, 1.0],
|
[576, 0.97873515, 0.008272092578813124, 1.0],
|
||||||
|
[538, 0.89114505, 0.01062259019889307, 1.0],
|
||||||
|
[436, 0.3119122, 0.01127515110543434, 1.0]
|
||||||
]
|
]
|
||||||
self.compare_diffexp_results(results, expects)
|
|
||||||
|
self.compare_diffexp_results(results['positive'], positive_expects)
|
||||||
|
self.compare_diffexp_results(results['negative'], negative_expects)
|
||||||
|
|
||||||
def get_X_col(self, adaptor, cols):
|
def get_X_col(self, adaptor, cols):
|
||||||
varmask = np.zeros(adaptor.get_shape()[1], dtype=bool)
|
varmask = np.zeros(adaptor.get_shape()[1], dtype=bool)
|
||||||
@@ -80,6 +97,7 @@ class DiffExpTest(unittest.TestCase):
|
|||||||
self.check_1_10_2_10(results)
|
self.check_1_10_2_10(results)
|
||||||
|
|
||||||
# run it directly
|
# run it directly
|
||||||
|
|
||||||
results = diffexp_ttest(adaptor, maskA, maskB, 10)
|
results = diffexp_ttest(adaptor, maskA, maskB, 10)
|
||||||
self.check_1_10_2_10(results)
|
self.check_1_10_2_10(results)
|
||||||
|
|
||||||
@@ -128,15 +146,22 @@ class DiffExpTest(unittest.TestCase):
|
|||||||
diffexp_results_sparse = diffexp_cxg.diffexp_ttest(adaptor_sparse, maskA, maskB, 10)
|
diffexp_results_sparse = diffexp_cxg.diffexp_ttest(adaptor_sparse, maskA, maskB, 10)
|
||||||
diffexp_results_dense = diffexp_cxg.diffexp_ttest(adaptor_dense, maskA, maskB, 10)
|
diffexp_results_dense = diffexp_cxg.diffexp_ttest(adaptor_dense, maskA, maskB, 10)
|
||||||
|
|
||||||
self.compare_diffexp_results(diffexp_results_anndata, diffexp_results_sparse)
|
self.compare_diffexp_results(diffexp_results_anndata['positive'], diffexp_results_sparse['positive'])
|
||||||
self.compare_diffexp_results(diffexp_results_anndata, diffexp_results_dense)
|
self.compare_diffexp_results(diffexp_results_anndata['negative'], diffexp_results_sparse['negative'])
|
||||||
|
|
||||||
|
self.compare_diffexp_results(diffexp_results_anndata['positive'], diffexp_results_dense['positive'])
|
||||||
|
self.compare_diffexp_results(diffexp_results_anndata['negative'], diffexp_results_dense['negative'])
|
||||||
|
|
||||||
|
topcols_pos = np.array([x[0] for x in diffexp_results_anndata['positive']])
|
||||||
|
topcols_neg = np.array([x[0] for x in diffexp_results_anndata['negative']])
|
||||||
|
topcols = np.concatenate((topcols_pos, topcols_neg))
|
||||||
|
|
||||||
topcols = np.array([x[0] for x in diffexp_results_anndata])
|
|
||||||
cols_anndata = self.get_X_col(adaptor_anndata, topcols)
|
cols_anndata = self.get_X_col(adaptor_anndata, topcols)
|
||||||
cols_sparse = self.get_X_col(adaptor_sparse, topcols)
|
cols_sparse = self.get_X_col(adaptor_sparse, topcols)
|
||||||
cols_dense = self.get_X_col(adaptor_dense, topcols)
|
cols_dense = self.get_X_col(adaptor_dense, topcols)
|
||||||
|
|
||||||
assert cols_anndata.shape[0] == adaptor_sparse.get_shape()[0]
|
assert cols_anndata.shape[0] == adaptor_sparse.get_shape()[0]
|
||||||
assert cols_anndata.shape[1] == len(diffexp_results_anndata)
|
assert cols_anndata.shape[1] == len(diffexp_results_anndata['positive']) + len(diffexp_results_anndata['negative'])
|
||||||
|
|
||||||
def convert(mat, cols):
|
def convert(mat, cols):
|
||||||
return decode_matrix_fbs(encode_matrix_fbs(mat, col_idx=cols)).to_numpy()
|
return decode_matrix_fbs(encode_matrix_fbs(mat, col_idx=cols)).to_numpy()
|
||||||
|
|||||||
@@ -152,9 +152,11 @@ class AdaptorTest(unittest.TestCase):
|
|||||||
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
||||||
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
||||||
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"]))
|
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"]))
|
||||||
self.assertEqual(len(result), 10)
|
self.assertEqual(len(result['positive']), 10)
|
||||||
|
self.assertEqual(len(result['negative']), 10)
|
||||||
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"], 20))
|
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"], 20))
|
||||||
self.assertEqual(len(result), 20)
|
self.assertEqual(len(result['positive']), 20)
|
||||||
|
self.assertEqual(len(result['negative']), 20)
|
||||||
|
|
||||||
def test_data_frame(self):
|
def test_data_frame(self):
|
||||||
f1 = {"var": {"index": [[0, 10]]}}
|
f1 = {"var": {"index": [[0, 10]]}}
|
||||||
|
|||||||
@@ -30,10 +30,15 @@ class DataLoadAdaptorTest(unittest.TestCase):
|
|||||||
def test_diffexp_topN(self):
|
def test_diffexp_topN(self):
|
||||||
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
||||||
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
||||||
|
|
||||||
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"]))
|
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"]))
|
||||||
self.assertEqual(len(result), 10)
|
|
||||||
|
self.assertEqual(len(result['positive']), 10)
|
||||||
|
self.assertEqual(len(result['negative']), 10)
|
||||||
|
|
||||||
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"], 20))
|
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"], 20))
|
||||||
self.assertEqual(len(result), 20)
|
self.assertEqual(len(result['positive']), 20)
|
||||||
|
self.assertEqual(len(result['negative']), 20)
|
||||||
|
|
||||||
|
|
||||||
class DataLocatorAdaptorTest(unittest.TestCase):
|
class DataLocatorAdaptorTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import random
|
|||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import backend.server.compute.diffexp_generic as diffexp_generic
|
import backend.common.compute.diffexp_generic as diffexp_generic
|
||||||
|
|
||||||
from backend.server.common.config.app_config import AppConfig
|
from backend.server.common.config.app_config import AppConfig
|
||||||
from backend.server.data_common.matrix_loader import MatrixDataLoader
|
from backend.server.data_common.matrix_loader import MatrixDataLoader
|
||||||
|
|||||||
@@ -414,7 +414,8 @@ class EndPointsAnndata(unittest.TestCase, EndPoints):
|
|||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
self.assertEqual(result.headers["Content-Type"], "application/json")
|
||||||
result_data = result.json()
|
result_data = result.json()
|
||||||
self.assertEqual(len(result_data), 7)
|
self.assertEqual(len(result_data['positive']), 7)
|
||||||
|
self.assertEqual(len(result_data['negative']), 7)
|
||||||
|
|
||||||
def test_diff_exp_indices(self):
|
def test_diff_exp_indices(self):
|
||||||
endpoint = "diffexp/obs"
|
endpoint = "diffexp/obs"
|
||||||
@@ -429,7 +430,8 @@ class EndPointsAnndata(unittest.TestCase, EndPoints):
|
|||||||
self.assertEqual(result.status_code, HTTPStatus.OK)
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
self.assertEqual(result.headers["Content-Type"], "application/json")
|
self.assertEqual(result.headers["Content-Type"], "application/json")
|
||||||
result_data = result.json()
|
result_data = result.json()
|
||||||
self.assertEqual(len(result_data), 10)
|
self.assertEqual(len(result_data['positive']), 10)
|
||||||
|
self.assertEqual(len(result_data['negative']), 10)
|
||||||
|
|
||||||
def test_get_summaryvar(self):
|
def test_get_summaryvar(self):
|
||||||
index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
||||||
|
|||||||
+47
-5
@@ -2,13 +2,14 @@ import unittest
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from backend.common.compute import diffexp_generic
|
||||||
from backend.server.data_common.matrix_loader import MatrixDataLoader
|
from backend.server.data_common.matrix_loader import MatrixDataLoader
|
||||||
from backend.test.test_server.unit import app_config
|
from backend.test.test_server.unit import app_config
|
||||||
from backend.test import PROJECT_ROOT
|
from backend.test import PROJECT_ROOT
|
||||||
|
|
||||||
|
|
||||||
class DiffExpTest(unittest.TestCase):
|
class DiffExpTest(unittest.TestCase):
|
||||||
"""Tests the diffexp returns the expected results for one test case, using different
|
"""Tests the diffexp returns the expected results for one test case, using the h5ad
|
||||||
adaptor types and different algorithms."""
|
adaptor types and different algorithms."""
|
||||||
|
|
||||||
def load_dataset(self, path, extra_server_config={}, extra_dataset_config={}):
|
def load_dataset(self, path, extra_server_config={}, extra_dataset_config={}):
|
||||||
@@ -35,19 +36,34 @@ class DiffExpTest(unittest.TestCase):
|
|||||||
|
|
||||||
def check_1_10_2_10(self, results):
|
def check_1_10_2_10(self, results):
|
||||||
"""Checks the results for a specific set of rows selections"""
|
"""Checks the results for a specific set of rows selections"""
|
||||||
expects = [
|
|
||||||
|
positive_expects = [
|
||||||
|
[1712, -0.5525154, 0.0051788902660723345, 1.0],
|
||||||
|
[1575, 1.0317602, 0.007830310753043345, 1.0],
|
||||||
|
[693, 0.4703904, 0.008715846769131548, 1.0],
|
||||||
|
[916, 0.9567287, 0.009080596532247588, 1.0],
|
||||||
|
[77, 0.02665649, 0.010070392939027756, 1.0],
|
||||||
|
[782, -1.0981874, 0.010161745218916036, 1.0],
|
||||||
|
[913, 0.5683986, 0.010782030711612685, 1.0],
|
||||||
|
[910, 0.83164597, 0.014596411069229197, 1.0],
|
||||||
|
[1727, 0.4127781, 0.015168372104237176, 1.0],
|
||||||
|
[1443, -0.8241895, 0.015337080567465522, 1.0]
|
||||||
|
]
|
||||||
|
negative_expects = [
|
||||||
[956, 0.016060986, 0.0008649321884808977, 1.0],
|
[956, 0.016060986, 0.0008649321884808977, 1.0],
|
||||||
[1124, 0.96602094, 0.0011717216548271284, 1.0],
|
[1124, 0.96602094, 0.0011717216548271284, 1.0],
|
||||||
[1809, 1.1110606, 0.0019304405196777848, 1.0],
|
[1809, 1.1110606, 0.0019304405196777848, 1.0],
|
||||||
[1712, -0.5525154, 0.0051788902660723345, 1.0],
|
|
||||||
[1754, 0.5201581, 0.005691734062127954, 1.0],
|
[1754, 0.5201581, 0.005691734062127954, 1.0],
|
||||||
[948, 1.6390722, 0.006622111055981219, 1.0],
|
[948, 1.6390722, 0.006622111055981219, 1.0],
|
||||||
[1810, 0.78618884, 0.007055917428377063, 1.0],
|
[1810, 0.78618884, 0.007055917428377063, 1.0],
|
||||||
[779, 1.5241305, 0.007202934422407284, 1.0],
|
[779, 1.5241305, 0.007202934422407284, 1.0],
|
||||||
[1575, 1.0317602, 0.007830310753043345, 1.0],
|
|
||||||
[576, 0.97873515, 0.008272092578813124, 1.0],
|
[576, 0.97873515, 0.008272092578813124, 1.0],
|
||||||
|
[538, 0.89114505, 0.01062259019889307, 1.0],
|
||||||
|
[436, 0.3119122, 0.01127515110543434, 1.0]
|
||||||
]
|
]
|
||||||
self.compare_diffexp_results(results, expects)
|
|
||||||
|
self.compare_diffexp_results(results["positive"], positive_expects)
|
||||||
|
self.compare_diffexp_results(results["negative"], negative_expects)
|
||||||
|
|
||||||
def get_X_col(self, adaptor, cols):
|
def get_X_col(self, adaptor, cols):
|
||||||
varmask = np.zeros(adaptor.get_shape()[1], dtype=bool)
|
varmask = np.zeros(adaptor.get_shape()[1], dtype=bool)
|
||||||
@@ -61,3 +77,29 @@ class DiffExpTest(unittest.TestCase):
|
|||||||
maskB = self.get_mask(adaptor, 2, 10)
|
maskB = self.get_mask(adaptor, 2, 10)
|
||||||
results = adaptor.compute_diffexp_ttest(maskA, maskB, 10)
|
results = adaptor.compute_diffexp_ttest(maskA, maskB, 10)
|
||||||
self.check_1_10_2_10(results)
|
self.check_1_10_2_10(results)
|
||||||
|
|
||||||
|
|
||||||
|
def test_h5ad_default(self):
|
||||||
|
"""Test a h5ad adaptor with its default diffexp algorithm (diffexp_cxg)"""
|
||||||
|
adaptor = self.load_dataset(f"{PROJECT_ROOT}/example-dataset/pbmc3k.h5ad")
|
||||||
|
maskA = self.get_mask(adaptor, 1, 10)
|
||||||
|
maskB = self.get_mask(adaptor, 2, 10)
|
||||||
|
|
||||||
|
# run it through the adaptor
|
||||||
|
results = adaptor.compute_diffexp_ttest(maskA, maskB, 10)
|
||||||
|
self.check_1_10_2_10(results)
|
||||||
|
|
||||||
|
# run it directly
|
||||||
|
|
||||||
|
results = diffexp_generic.diffexp_ttest(adaptor, maskA, maskB, 10)
|
||||||
|
self.check_1_10_2_10(results)
|
||||||
|
|
||||||
|
|
||||||
|
def test_h5ad_generic(self):
|
||||||
|
"""Test a h5ad adaptor with the generic adaptor"""
|
||||||
|
adaptor = self.load_dataset(f"{PROJECT_ROOT}/example-dataset/pbmc3k.h5ad")
|
||||||
|
maskA = self.get_mask(adaptor, 1, 10)
|
||||||
|
maskB = self.get_mask(adaptor, 2, 10)
|
||||||
|
# run it directly
|
||||||
|
results = diffexp_generic.diffexp_ttest(adaptor, maskA, maskB, 10)
|
||||||
|
self.check_1_10_2_10(results)
|
||||||
@@ -153,9 +153,12 @@ class AdaptorTest(unittest.TestCase):
|
|||||||
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
||||||
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
||||||
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"]))
|
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"]))
|
||||||
self.assertEqual(len(result), 10)
|
self.assertEqual(len(result['positive']), 10)
|
||||||
|
self.assertEqual(len(result['negative']), 10)
|
||||||
|
|
||||||
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"], 20))
|
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"], 20))
|
||||||
self.assertEqual(len(result), 20)
|
self.assertEqual(len(result['positive']), 20)
|
||||||
|
self.assertEqual(len(result['negative']), 20)
|
||||||
|
|
||||||
def test_data_frame(self):
|
def test_data_frame(self):
|
||||||
f1 = {"var": {"index": [[0, 10]]}}
|
f1 = {"var": {"index": [[0, 10]]}}
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ class DataLoadAdaptorTest(unittest.TestCase):
|
|||||||
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
f1 = {"filter": {"obs": {"index": [[0, 500]]}}}
|
||||||
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
f2 = {"filter": {"obs": {"index": [[500, 1000]]}}}
|
||||||
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"]))
|
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"]))
|
||||||
self.assertEqual(len(result), 10)
|
self.assertEqual(len(result['positive']), 10)
|
||||||
|
self.assertEqual(len(result['negative']), 10)
|
||||||
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"], 20))
|
result = json.loads(self.data.diffexp_topN(f1["filter"], f2["filter"], 20))
|
||||||
self.assertEqual(len(result), 20)
|
self.assertEqual(len(result['positive']), 20)
|
||||||
|
self.assertEqual(len(result['negative']), 20)
|
||||||
|
|
||||||
|
|
||||||
class DataLocatorAdaptorTest(unittest.TestCase):
|
class DataLocatorAdaptorTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -211,15 +211,18 @@ const requestDifferentialExpression = (set1, set2, num_genes = 50) => async (
|
|||||||
|
|
||||||
const response = await res.json();
|
const response = await res.json();
|
||||||
const varIndex = await annoMatrix.fetch("var", varIndexName);
|
const varIndex = await annoMatrix.fetch("var", varIndexName);
|
||||||
const data = response.map((v) => [
|
const diffexpLists = { negative: [], positive: [] };
|
||||||
varIndex.at(v[0], varIndexName),
|
for (const polarity of Object.keys(diffexpLists)) {
|
||||||
...v.slice(1),
|
diffexpLists[polarity] = response[polarity].map((v) => [
|
||||||
]);
|
varIndex.at(v[0], varIndexName),
|
||||||
|
...v.slice(1),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/* then send the success case action through */
|
/* then send the success case action through */
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: "request differential expression success",
|
type: "request differential expression success",
|
||||||
data,
|
data: diffexpLists,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return dispatch({
|
return dispatch({
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ const Histogram = ({
|
|||||||
/*
|
/*
|
||||||
Create the d3 histogram
|
Create the d3 histogram
|
||||||
*/
|
*/
|
||||||
const { marginLeft, marginRight, marginBottom, marginTop } = margin;
|
// This is just a constant that's flipped by parent's `mini` boolean
|
||||||
|
const {
|
||||||
|
LEFT: marginLeft,
|
||||||
|
RIGHT: marginRight,
|
||||||
|
BOTTOM: marginBottom,
|
||||||
|
TOP: marginTop,
|
||||||
|
} = margin;
|
||||||
const { x, y, bins, binStart, binEnd, binWidth } = histogram;
|
const { x, y, bins, binStart, binEnd, binWidth } = histogram;
|
||||||
const svg = d3.select(svgRef.current);
|
const svg = d3.select(svgRef.current);
|
||||||
const binPadding = mini ? 0 : -1;
|
const binPadding = mini ? 0 : -1;
|
||||||
|
|||||||
@@ -13,6 +13,23 @@ import HistogramFooter from "./footer";
|
|||||||
import StillLoading from "./loading";
|
import StillLoading from "./loading";
|
||||||
import ErrorLoading from "./error";
|
import ErrorLoading from "./error";
|
||||||
|
|
||||||
|
const MARGIN = {
|
||||||
|
LEFT: 10, // Space for 0 tick label on X axis
|
||||||
|
RIGHT: 54, // space for Y axis & labels
|
||||||
|
BOTTOM: 25, // space for X axis & labels
|
||||||
|
TOP: 3,
|
||||||
|
};
|
||||||
|
const WIDTH = 340 - MARGIN.LEFT - MARGIN.RIGHT;
|
||||||
|
const HEIGHT = 135 - MARGIN.TOP - MARGIN.BOTTOM;
|
||||||
|
const MARGIN_MINI = {
|
||||||
|
LEFT: 0, // Space for 0 tick label on X axis
|
||||||
|
RIGHT: 0, // space for Y axis & labels
|
||||||
|
BOTTOM: 0, // space for X axis & labels
|
||||||
|
TOP: 0,
|
||||||
|
};
|
||||||
|
const WIDTH_MINI = 120 - MARGIN_MINI.LEFT - MARGIN_MINI.RIGHT;
|
||||||
|
const HEIGHT_MINI = 15 - MARGIN_MINI.TOP - MARGIN_MINI.BOTTOM;
|
||||||
|
|
||||||
@connect((state, ownProps) => {
|
@connect((state, ownProps) => {
|
||||||
const { isObs, isUserDefined, isGeneSetSummary, field } = ownProps;
|
const { isObs, isUserDefined, isGeneSetSummary, field } = ownProps;
|
||||||
const myName = makeContinuousDimensionName(
|
const myName = makeContinuousDimensionName(
|
||||||
@@ -44,34 +61,6 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const marginLeft = 10; // Space for 0 tick label on X axis
|
|
||||||
const marginRight = 54; // space for Y axis & labels
|
|
||||||
const marginBottom = 25; // space for X axis & labels
|
|
||||||
const marginTop = 3;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
margin: {
|
|
||||||
marginLeft,
|
|
||||||
marginRight,
|
|
||||||
marginBottom,
|
|
||||||
marginTop,
|
|
||||||
},
|
|
||||||
width: 340 - marginLeft - marginRight,
|
|
||||||
height: 135 - marginTop - marginBottom,
|
|
||||||
marginMini: {
|
|
||||||
marginLeft: 0, // Space for 0 tick label on X axis
|
|
||||||
marginRight: 0, // space for Y axis & labels
|
|
||||||
marginBottom: 0, // space for X axis & labels
|
|
||||||
marginTop: 0,
|
|
||||||
},
|
|
||||||
widthMini: 120,
|
|
||||||
heightMini: 15,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onBrush = (selection, x, eventType) => {
|
onBrush = (selection, x, eventType) => {
|
||||||
const type = `continuous metadata histogram ${eventType}`;
|
const type = `continuous metadata histogram ${eventType}`;
|
||||||
return () => {
|
return () => {
|
||||||
@@ -210,15 +199,8 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchAsyncProps = async () => {
|
fetchAsyncProps = async () => {
|
||||||
const { annoMatrix } = this.props;
|
const { annoMatrix, width } = this.props;
|
||||||
const {
|
|
||||||
margin,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
marginMini,
|
|
||||||
widthMini,
|
|
||||||
heightMini,
|
|
||||||
} = this.state;
|
|
||||||
const { isClipped } = annoMatrix;
|
const { isClipped } = annoMatrix;
|
||||||
|
|
||||||
const query = this.createQuery();
|
const query = this.createQuery();
|
||||||
@@ -246,12 +228,17 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
: globals.blue,
|
: globals.blue,
|
||||||
];
|
];
|
||||||
|
|
||||||
const histogram = this.calcHistogramCache(column, margin, width, height);
|
const histogram = this.calcHistogramCache(
|
||||||
|
column,
|
||||||
|
MARGIN,
|
||||||
|
width || WIDTH,
|
||||||
|
HEIGHT
|
||||||
|
);
|
||||||
const miniHistogram = this.calcHistogramCache(
|
const miniHistogram = this.calcHistogramCache(
|
||||||
column,
|
column,
|
||||||
marginMini,
|
MARGIN_MINI,
|
||||||
widthMini,
|
width || WIDTH_MINI,
|
||||||
heightMini
|
HEIGHT_MINI
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSingleValue = summary.min === summary.max;
|
const isSingleValue = summary.min === summary.max;
|
||||||
@@ -275,7 +262,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this -- instance method allows for memoization per annotation
|
// eslint-disable-next-line class-methods-use-this -- instance method allows for memoization per annotation
|
||||||
calcHistogramCache(col, margin, width, height) {
|
calcHistogramCache(col, newMargin, newWidth, newHeight) {
|
||||||
/*
|
/*
|
||||||
recalculate expensive stuff, notably bins, summaries, etc.
|
recalculate expensive stuff, notably bins, summaries, etc.
|
||||||
*/
|
*/
|
||||||
@@ -283,7 +270,10 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
const summary = col.summarize(); /* this is memoized, so it's free the second time you call it */
|
const summary = col.summarize(); /* this is memoized, so it's free the second time you call it */
|
||||||
const { min: domainMin, max: domainMax } = summary;
|
const { min: domainMin, max: domainMax } = summary;
|
||||||
const numBins = 40;
|
const numBins = 40;
|
||||||
const { marginTop, marginLeft } = margin; /* changes with mini */
|
const {
|
||||||
|
TOP: topMargin,
|
||||||
|
LEFT: leftMargin,
|
||||||
|
} = newMargin; /* changes with mini */
|
||||||
|
|
||||||
histogramCache.domain = [
|
histogramCache.domain = [
|
||||||
domainMin,
|
domainMin,
|
||||||
@@ -293,7 +283,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
histogramCache.x = d3
|
histogramCache.x = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.domain([domainMin, domainMax])
|
.domain([domainMin, domainMax])
|
||||||
.range([marginLeft, marginLeft + width]);
|
.range([leftMargin, leftMargin + newWidth]);
|
||||||
|
|
||||||
histogramCache.bins = histogramContinuous(col, numBins, [
|
histogramCache.bins = histogramContinuous(col, numBins, [
|
||||||
domainMin,
|
domainMin,
|
||||||
@@ -310,7 +300,7 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
histogramCache.y = d3
|
histogramCache.y = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.domain([0, yMax])
|
.domain([0, yMax])
|
||||||
.range([marginTop + height, marginTop]);
|
.range([topMargin + newHeight, topMargin]);
|
||||||
|
|
||||||
return histogramCache;
|
return histogramCache;
|
||||||
}
|
}
|
||||||
@@ -367,14 +357,12 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
mini,
|
mini,
|
||||||
setGenes,
|
setGenes,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {
|
|
||||||
margin,
|
let { width } = this.props;
|
||||||
width,
|
if (!width) {
|
||||||
height,
|
width = mini ? WIDTH_MINI : WIDTH;
|
||||||
marginMini,
|
}
|
||||||
widthMini,
|
|
||||||
heightMini,
|
|
||||||
} = this.state;
|
|
||||||
const fieldForId = field.replace(/\s/g, "_");
|
const fieldForId = field.replace(/\s/g, "_");
|
||||||
const showScatterPlot = isUserDefined;
|
const showScatterPlot = isUserDefined;
|
||||||
|
|
||||||
@@ -432,11 +420,11 @@ class HistogramBrush extends React.PureComponent {
|
|||||||
histogram={
|
histogram={
|
||||||
mini ? asyncProps.miniHistogram : asyncProps.histogram
|
mini ? asyncProps.miniHistogram : asyncProps.histogram
|
||||||
}
|
}
|
||||||
width={mini ? widthMini : width}
|
width={width}
|
||||||
height={mini ? heightMini : height}
|
height={mini ? HEIGHT_MINI : HEIGHT}
|
||||||
onBrush={this.onBrush}
|
onBrush={this.onBrush}
|
||||||
onBrushEnd={this.onBrushEnd}
|
onBrushEnd={this.onBrushEnd}
|
||||||
margin={mini ? marginMini : margin}
|
margin={mini ? MARGIN_MINI : MARGIN}
|
||||||
isColorBy={isColorAccessor}
|
isColorBy={isColorAccessor}
|
||||||
selectionRange={continuousSelectionRange}
|
selectionRange={continuousSelectionRange}
|
||||||
mini={mini}
|
mini={mini}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
import { AnchorButton, Icon } from "@blueprintjs/core";
|
import { Button, Icon } from "@blueprintjs/core";
|
||||||
import Truncate from "../util/truncate";
|
import Truncate from "../util/truncate";
|
||||||
import HistogramBrush from "../brushableHistogram";
|
import HistogramBrush from "../brushableHistogram";
|
||||||
|
|
||||||
import * as globals from "../../globals";
|
|
||||||
import actions from "../../actions";
|
import actions from "../../actions";
|
||||||
|
|
||||||
|
const MINI_HISTOGRAM_WIDTH = 110;
|
||||||
|
|
||||||
@connect((state, ownProps) => {
|
@connect((state, ownProps) => {
|
||||||
const { gene } = ownProps;
|
const { gene } = ownProps;
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class Gene extends React.Component {
|
|||||||
isScatterplotYYaccessor,
|
isScatterplotYYaccessor,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { geneIsExpanded } = this.state;
|
const { geneIsExpanded } = this.state;
|
||||||
const genesetNameLengthVisible = 310; /* this magic number determines how much of a long geneset name we see */
|
const geneSymbolWidth = 60 + (geneIsExpanded ? MINI_HISTOGRAM_WIDTH : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -108,7 +109,8 @@ class Gene extends React.Component {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
width: globals.leftSidebarWidth - genesetNameLengthVisible,
|
width: geneSymbolWidth,
|
||||||
|
display: "inline-block",
|
||||||
}}
|
}}
|
||||||
data-testid={`${gene}:gene-label`}
|
data-testid={`${gene}:gene-label`}
|
||||||
>
|
>
|
||||||
@@ -117,11 +119,16 @@ class Gene extends React.Component {
|
|||||||
</Truncate>
|
</Truncate>
|
||||||
</div>
|
</div>
|
||||||
{!geneIsExpanded ? (
|
{!geneIsExpanded ? (
|
||||||
<HistogramBrush isUserDefined field={gene} mini />
|
<HistogramBrush
|
||||||
|
isUserDefined
|
||||||
|
field={gene}
|
||||||
|
mini
|
||||||
|
width={MINI_HISTOGRAM_WIDTH}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flexShrink: 0, marginLeft: 2 }}>
|
<div style={{ flexShrink: 0, marginLeft: 2 }}>
|
||||||
<AnchorButton
|
<Button
|
||||||
minimal
|
minimal
|
||||||
small
|
small
|
||||||
data-testid={`delete-from-geneset-${gene}`}
|
data-testid={`delete-from-geneset-${gene}`}
|
||||||
@@ -130,8 +137,7 @@ class Gene extends React.Component {
|
|||||||
style={{ fontWeight: 700, marginRight: 2 }}
|
style={{ fontWeight: 700, marginRight: 2 }}
|
||||||
icon={<Icon icon="trash" iconSize={10} />}
|
icon={<Icon icon="trash" iconSize={10} />}
|
||||||
/>
|
/>
|
||||||
)
|
<Button
|
||||||
<AnchorButton
|
|
||||||
minimal
|
minimal
|
||||||
small
|
small
|
||||||
data-testid={`plot-x-${gene}`}
|
data-testid={`plot-x-${gene}`}
|
||||||
@@ -141,8 +147,8 @@ class Gene extends React.Component {
|
|||||||
style={{ fontWeight: 700, marginRight: 2 }}
|
style={{ fontWeight: 700, marginRight: 2 }}
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
</AnchorButton>
|
</Button>
|
||||||
<AnchorButton
|
<Button
|
||||||
minimal
|
minimal
|
||||||
small
|
small
|
||||||
data-testid={`plot-y-${gene}`}
|
data-testid={`plot-y-${gene}`}
|
||||||
@@ -152,8 +158,8 @@ class Gene extends React.Component {
|
|||||||
style={{ fontWeight: 700, marginRight: 2 }}
|
style={{ fontWeight: 700, marginRight: 2 }}
|
||||||
>
|
>
|
||||||
y
|
y
|
||||||
</AnchorButton>
|
</Button>
|
||||||
<AnchorButton
|
<Button
|
||||||
minimal
|
minimal
|
||||||
small
|
small
|
||||||
data-testclass="maximize"
|
data-testclass="maximize"
|
||||||
@@ -164,7 +170,7 @@ class Gene extends React.Component {
|
|||||||
icon={<Icon icon="maximize" iconSize={10} />}
|
icon={<Icon icon="maximize" iconSize={10} />}
|
||||||
style={{ marginRight: 2 }}
|
style={{ marginRight: 2 }}
|
||||||
/>
|
/>
|
||||||
<AnchorButton
|
<Button
|
||||||
minimal
|
minimal
|
||||||
small
|
small
|
||||||
data-testclass="colorby"
|
data-testclass="colorby"
|
||||||
|
|||||||
@@ -96,12 +96,17 @@ const GeneSets = (
|
|||||||
if (state.genesets.has(genesetName))
|
if (state.genesets.has(genesetName))
|
||||||
throw new Error("geneset: create -- name already defined.");
|
throw new Error("geneset: create -- name already defined.");
|
||||||
|
|
||||||
const genesets = new Map(state.genesets); // clone
|
const genesets = new Map([
|
||||||
genesets.set(genesetName, {
|
[
|
||||||
genesetName,
|
genesetName,
|
||||||
genesetDescription,
|
{
|
||||||
genes: new Map(),
|
genesetName,
|
||||||
});
|
genesetDescription,
|
||||||
|
genes: new Map(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...state.genesets,
|
||||||
|
]); // clone and add new geneset to beginning
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -357,23 +362,34 @@ const GeneSets = (
|
|||||||
case "request differential expression success": {
|
case "request differential expression success": {
|
||||||
const { data } = action;
|
const { data } = action;
|
||||||
|
|
||||||
const genes = new Map(
|
const dateString = new Date().toLocaleString();
|
||||||
data.map((diffExpGene) => [
|
|
||||||
diffExpGene[0],
|
const genesetNames = {
|
||||||
|
positive: `Pop1 high (${dateString})`,
|
||||||
|
negative: `Pop2 high (${dateString})`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const diffExpGeneSets = [];
|
||||||
|
for (const polarity of Object.keys(genesetNames)) {
|
||||||
|
const genes = new Map(
|
||||||
|
data[polarity].map((diffExpGene) => [
|
||||||
|
diffExpGene[0],
|
||||||
|
{
|
||||||
|
geneSymbol: diffExpGene[0],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
diffExpGeneSets.push([
|
||||||
|
genesetNames[polarity],
|
||||||
{
|
{
|
||||||
geneSymbol: diffExpGene[0],
|
genesetName: genesetNames[polarity],
|
||||||
|
genesetDescription: "",
|
||||||
|
genes,
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
);
|
}
|
||||||
|
|
||||||
const genesetName = `DiffExp Set (${new Date().toLocaleString()})`;
|
const genesets = new Map([...diffExpGeneSets, ...state.genesets]); // clone
|
||||||
|
|
||||||
const genesets = new Map(state.genesets); // clone
|
|
||||||
genesets.set(genesetName, {
|
|
||||||
genesetName,
|
|
||||||
genesetDescription: "",
|
|
||||||
genes,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
Reference in New Issue
Block a user