mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-16 13:27:59 +08:00
* Improve diffexp for tiledb - The rows from the A and B sets are gathered and processed at the same time. In this way the matrix is only accessed once instead of twice for each tile. - There is now a single thread queue that gets shared between all callers of the diffexp. This will slow down work if diffexp gets too busy. - There is a target_workunit amount of work given to each thread. Previously the workunit was (rows selected * width of tile), which could be small. Now multiple column tiles can be combined into one workunit. If the target is too small then thread and other overheads may reduce performance. If target_workunit is too large then the size of the gathered sub matrix may take up too much memory. - add configuration parameters (max_workers, cpu_multiplier, and target_workunit)
558 lines
25 KiB
Python
558 lines
25 KiB
Python
from server import __version__ as cellxgene_version
|
|
from flatten_dict import flatten
|
|
import os
|
|
from os.path import splitext, basename, isdir
|
|
import sys
|
|
from urllib.parse import urlparse
|
|
import yaml
|
|
import copy
|
|
import boto3
|
|
import botocore
|
|
|
|
from server.common.default_config import get_default_config
|
|
from server.common.errors import ConfigurationError, DatasetAccessError, OntologyLoadFailure
|
|
from server.data_common.matrix_loader import MatrixDataLoader, MatrixDataCacheManager, MatrixDataType
|
|
from server.common.utils import find_available_port, is_port_available
|
|
import warnings
|
|
from server.common.annotations import AnnotationsLocalFile
|
|
from server.common.utils import custom_format_warning
|
|
import server.compute.diffexp_cxg as diffexp_tiledb
|
|
|
|
DEFAULT_SERVER_PORT = int(os.environ.get("CXG_SERVER_PORT", "5005"))
|
|
# anything bigger than this will generate a special message
|
|
BIG_FILE_SIZE_THRESHOLD = 100 * 2 ** 20 # 100MB
|
|
|
|
|
|
class AppFeature(object):
|
|
def __init__(self, path, available=False, method="POST", extra={}):
|
|
self.path = path
|
|
self.available = available
|
|
self.method = method
|
|
self.extra = extra
|
|
for k, v in extra.items():
|
|
setattr(self, k, v)
|
|
|
|
def todict(self):
|
|
d = dict(available=self.available, method=self.method, path=self.path)
|
|
d.update(self.extra)
|
|
return d
|
|
|
|
|
|
class AppConfig(object):
|
|
def __init__(self):
|
|
|
|
self.default_config = get_default_config()
|
|
self.attr_checked = {k: False for k in self.__mapping(self.default_config).keys()}
|
|
|
|
dc = self.default_config
|
|
try:
|
|
self.server__verbose = dc["server"]["verbose"]
|
|
self.server__debug = dc["server"]["debug"]
|
|
self.server__host = dc["server"]["host"]
|
|
self.server__port = dc["server"]["port"]
|
|
self.server__scripts = dc["server"]["scripts"]
|
|
self.server__open_browser = dc["server"]["open_browser"]
|
|
self.server__about_legal_tos = dc["server"]["about_legal_tos"]
|
|
self.server__about_legal_privacy = dc["server"]["about_legal_privacy"]
|
|
self.server__force_https = dc["server"]["force_https"]
|
|
self.server__flask_secret_key = dc["server"]["flask_secret_key"]
|
|
self.server__generate_cache_control_headers = dc["server"]["generate_cache_control_headers"]
|
|
self.server__server_timing_headers = dc["server"]["server_timing_headers"]
|
|
|
|
self.multi_dataset__dataroot = dc["multi_dataset"]["dataroot"]
|
|
self.multi_dataset__index = dc["multi_dataset"]["index"]
|
|
self.multi_dataset__allowed_matrix_types = dc["multi_dataset"]["allowed_matrix_types"]
|
|
self.multi_dataset__matrix_cache__max_datasets = dc["multi_dataset"]["matrix_cache"]["max_datasets"]
|
|
self.multi_dataset__matrix_cache__timelimit_s = dc["multi_dataset"]["matrix_cache"]["timelimit_s"]
|
|
|
|
self.single_dataset__datapath = dc["single_dataset"]["datapath"]
|
|
self.single_dataset__obs_names = dc["single_dataset"]["obs_names"]
|
|
self.single_dataset__var_names = dc["single_dataset"]["var_names"]
|
|
self.single_dataset__about = dc["single_dataset"]["about"]
|
|
self.single_dataset__title = dc["single_dataset"]["title"]
|
|
|
|
self.user_annotations__enable = dc["user_annotations"]["enable"]
|
|
self.user_annotations__type = dc["user_annotations"]["type"]
|
|
self.user_annotations__local_file_csv__directory = dc["user_annotations"]["local_file_csv"]["directory"]
|
|
self.user_annotations__local_file_csv__file = dc["user_annotations"]["local_file_csv"]["file"]
|
|
self.user_annotations__ontology__enable = dc["user_annotations"]["ontology"]["enable"]
|
|
self.user_annotations__ontology__obo_location = dc["user_annotations"]["ontology"]["obo_location"]
|
|
|
|
self.presentation__max_categories = dc["presentation"]["max_categories"]
|
|
|
|
self.embeddings__names = dc["embeddings"]["names"]
|
|
self.embeddings__enable_reembedding = dc["embeddings"]["enable_reembedding"]
|
|
|
|
self.diffexp__enable = dc["diffexp"]["enable"]
|
|
self.diffexp__lfc_cutoff = dc["diffexp"]["lfc_cutoff"]
|
|
self.diffexp__top_n = dc["diffexp"]["top_n"]
|
|
self.diffexp__alg_cxg__max_workers = dc["diffexp"]["alg_cxg"]["max_workers"]
|
|
self.diffexp__alg_cxg__cpu_multiplier = dc["diffexp"]["alg_cxg"]["cpu_multiplier"]
|
|
self.diffexp__alg_cxg__target_workunit = dc["diffexp"]["alg_cxg"]["target_workunit"]
|
|
|
|
self.data_locator__s3__region_name = dc["data_locator"]["s3"]["region_name"]
|
|
|
|
self.adaptor__cxg_adaptor__tiledb_ctx = dc["adaptor"]["cxg_adaptor"]["tiledb_ctx"]
|
|
self.adaptor__anndata_adaptor__backed = dc["adaptor"]["anndata_adaptor"]["backed"]
|
|
|
|
self.limits__diffexp_cellcount_max = dc["limits"]["diffexp_cellcount_max"]
|
|
self.limits__column_request_max = dc["limits"]["column_request_max"]
|
|
|
|
except KeyError as e:
|
|
raise ConfigurationError(f"Unexpected config: {str(e)}")
|
|
|
|
# The annotation object is created during complete_config and stored here.
|
|
self.user_annotations = None
|
|
|
|
# The matrix data cache manager is created during the complete_config and stored here.
|
|
self.matrix_data_cache_manager = None
|
|
|
|
# Set to true when config_completed is called
|
|
self.is_completed = False
|
|
|
|
def check_config(self):
|
|
if not self.is_completed:
|
|
raise ConfigurationError("The configuration has not been completed")
|
|
mapping = self.__mapping(self.default_config)
|
|
for key in mapping.keys():
|
|
if not self.attr_checked[key]:
|
|
raise ConfigurationError(f"The attr '{key}' has not been checked")
|
|
|
|
def __mapping(self, config):
|
|
"""Create a mapping from attribute names to (location in the config tree, value)"""
|
|
dc = copy.deepcopy(config)
|
|
mapping = {}
|
|
|
|
# special case for tiledb_ctx whose value is a dict.
|
|
val = config.get("adaptor", {}).get("cxg_adaptor", {}).get("tiledb_ctx")
|
|
if val is not None:
|
|
mapping["adaptor__cxg_adaptor__tiledb_ctx"] = (("adaptor", "cxg_adaptor", "tiledb_ctx"), val)
|
|
del dc["adaptor"]["cxg_adaptor"]["tiledb_ctx"]
|
|
|
|
flat_config = flatten(dc)
|
|
for key, value in flat_config.items():
|
|
# name of the attribute
|
|
attr = "__".join(key)
|
|
mapping[attr] = (key, value)
|
|
|
|
return mapping
|
|
|
|
def update_from_config_file(self, config_file):
|
|
with open(config_file) as fyaml:
|
|
config = yaml.load(fyaml, Loader=yaml.FullLoader)
|
|
|
|
mapping = self.__mapping(config)
|
|
for attr, (key, value) in mapping.items():
|
|
if not hasattr(self, attr):
|
|
raise ConfigurationError(f"Unknown key from config file: {key}")
|
|
try:
|
|
setattr(self, attr, value)
|
|
except KeyError:
|
|
raise ConfigurationError(f"Unable to set config attribute: {key}")
|
|
|
|
self.attr_checked[attr] = False
|
|
|
|
self.is_completed = False
|
|
|
|
def update(self, **kw):
|
|
for key, value in kw.items():
|
|
if not hasattr(self, key):
|
|
raise ConfigurationError(f"unknown config parameter {key}.")
|
|
try:
|
|
setattr(self, key, value)
|
|
except KeyError:
|
|
raise ConfigurationError(f"Unable to set config parameter {key}.")
|
|
|
|
self.attr_checked[key] = False
|
|
|
|
self.is_completed = False
|
|
|
|
def changes_from_default(self):
|
|
"""Return all the attribute that are different from the default"""
|
|
mapping = self.__mapping(self.default_config)
|
|
diff = []
|
|
for attrname, (key, defval) in mapping.items():
|
|
curval = getattr(self, attrname)
|
|
if curval != defval:
|
|
diff.append((attrname, curval, defval))
|
|
return diff
|
|
|
|
def complete_config(self, messagefn=None):
|
|
"""The configure options are checked, and any additional setup based on the config
|
|
parameters is done"""
|
|
|
|
if messagefn is None:
|
|
|
|
def noop(message):
|
|
pass
|
|
|
|
messagefn = noop
|
|
|
|
# TODO: to give better error messages we can add a mapping between where each config
|
|
# attribute originated (e.g. command line argument or config file), then in the error
|
|
# messages we can give correct context for attributes with bad value.
|
|
context = dict(messagefn=messagefn)
|
|
|
|
self.handle_server(context)
|
|
self.handle_adaptor(context)
|
|
self.handle_data_locator(context)
|
|
self.handle_adaptor(context) # may depend on data_locator
|
|
self.handle_presentation(context)
|
|
self.handle_single_dataset(context) # may depend on adaptor
|
|
self.handle_multi_dataset(context) # may depend on adaptor
|
|
self.handle_user_annotations(context)
|
|
self.handle_embeddings(context)
|
|
self.handle_diffexp(context)
|
|
self.handle_limits(context)
|
|
|
|
self.is_completed = True
|
|
self.check_config()
|
|
|
|
def __check_attr(self, attrname, vtype):
|
|
val = getattr(self, attrname)
|
|
if type(vtype) in (list, tuple):
|
|
if type(val) not in vtype:
|
|
tnames = ",".join([x.__name__ for x in vtype])
|
|
raise ConfigurationError(
|
|
f"Invalid type for attribute: {attrname}, expected types ({tnames}), got {type(val).__name__}"
|
|
)
|
|
else:
|
|
if type(val) != vtype:
|
|
raise ConfigurationError(
|
|
f"Invalid type for attribute: {attrname}, "
|
|
f"expected type {vtype.__name__}, got {type(val).__name__}"
|
|
)
|
|
|
|
self.attr_checked[attrname] = True
|
|
|
|
def handle_server(self, context):
|
|
self.__check_attr("server__verbose", bool)
|
|
self.__check_attr("server__debug", bool)
|
|
self.__check_attr("server__host", str)
|
|
self.__check_attr("server__port", (type(None), int))
|
|
self.__check_attr("server__scripts", (list, tuple))
|
|
self.__check_attr("server__open_browser", bool)
|
|
self.__check_attr("server__force_https", bool)
|
|
self.__check_attr("server__flask_secret_key", (type(None), str))
|
|
self.__check_attr("server__generate_cache_control_headers", bool)
|
|
self.__check_attr("server__about_legal_tos", (type(None), str))
|
|
self.__check_attr("server__about_legal_privacy", (type(None), str))
|
|
self.__check_attr("server__server_timing_headers", bool)
|
|
|
|
if self.server__port:
|
|
if not is_port_available(self.server__host, self.server__port):
|
|
raise ConfigurationError(
|
|
f"The port selected {self.server__port} is in use, please configure an open port."
|
|
)
|
|
else:
|
|
self.server__port = find_available_port(self.server__host, DEFAULT_SERVER_PORT)
|
|
|
|
if self.server__debug:
|
|
context["messagefn"]("in debug mode, setting verbose=True and open_browser=False")
|
|
self.server__verbose = True
|
|
self.server__open_browser = False
|
|
else:
|
|
warnings.formatwarning = custom_format_warning
|
|
|
|
if not self.server__verbose:
|
|
sys.tracebacklimit = 0
|
|
|
|
# secret key:
|
|
# first, from CXG_SECRET_KEY environment variable
|
|
# second, from config file
|
|
self.server__flask_secret_key = os.environ.get("CXG_SECRET_KEY", self.server__flask_secret_key)
|
|
|
|
def handle_data_locator(self, context):
|
|
self.__check_attr("data_locator__s3__region_name", (type(None), bool, str))
|
|
if self.data_locator__s3__region_name is True:
|
|
path = self.single_dataset__datapath or self.multi_dataset__dataroot
|
|
if path and path.startswith("s3:"):
|
|
bucket = urlparse(path).netloc
|
|
client = boto3.client("s3")
|
|
try:
|
|
res = client.head_bucket(Bucket=bucket)
|
|
except botocore.exceptions.ClientError:
|
|
raise ConfigurationError(f"Unable to determine region from {path}")
|
|
|
|
region = res.get("ResponseMetadata", {}).get("HTTPHeaders", {}).get("x-amz-bucket-region")
|
|
if region:
|
|
self.data_locator__s3__region_name = region
|
|
else:
|
|
raise ConfigurationError(f"Unable to determine region from {path}")
|
|
|
|
def handle_presentation(self, context):
|
|
self.__check_attr("presentation__max_categories", int)
|
|
|
|
def handle_single_dataset(self, context):
|
|
self.__check_attr("single_dataset__datapath", (str, type(None)))
|
|
self.__check_attr("single_dataset__title", (str, type(None)))
|
|
self.__check_attr("single_dataset__about", (str, type(None)))
|
|
self.__check_attr("single_dataset__obs_names", (str, type(None)))
|
|
self.__check_attr("single_dataset__var_names", (str, type(None)))
|
|
|
|
if self.single_dataset__datapath is None:
|
|
if self.multi_dataset__dataroot is None:
|
|
# TODO: change the error message once dataroot is fully supported
|
|
raise ConfigurationError("missing datapath")
|
|
return
|
|
else:
|
|
if self.multi_dataset__dataroot is not None:
|
|
raise ConfigurationError("must supply only one of datapath or dataroot")
|
|
|
|
# create the matrix data cache manager:
|
|
if self.matrix_data_cache_manager is None:
|
|
self.matrix_data_cache_manager = MatrixDataCacheManager(max_cached=1, timelimit_s=None)
|
|
|
|
# preload this data set
|
|
matrix_data_loader = MatrixDataLoader(self.single_dataset__datapath, app_config=self)
|
|
try:
|
|
matrix_data_loader.pre_load_validation()
|
|
except DatasetAccessError as e:
|
|
raise ConfigurationError(str(e))
|
|
|
|
file_size = matrix_data_loader.file_size()
|
|
file_basename = basename(self.single_dataset__datapath)
|
|
if file_size > BIG_FILE_SIZE_THRESHOLD:
|
|
context["messagefn"](f"Loading data from {file_basename}, this may take a while...")
|
|
else:
|
|
context["messagefn"](f"Loading data from {file_basename}.")
|
|
|
|
if self.single_dataset__about:
|
|
|
|
def url_check(url):
|
|
try:
|
|
result = urlparse(url)
|
|
if all([result.scheme, result.netloc]):
|
|
return True
|
|
else:
|
|
return False
|
|
except ValueError:
|
|
return False
|
|
|
|
if not url_check(self.single_dataset__about):
|
|
raise ConfigurationError(
|
|
"Must provide an absolute URL for --about. (Example format: http://example.com)"
|
|
)
|
|
|
|
def handle_multi_dataset(self, context):
|
|
self.__check_attr("multi_dataset__dataroot", (type(None), str))
|
|
self.__check_attr("multi_dataset__index", (type(None), bool, str))
|
|
self.__check_attr("multi_dataset__allowed_matrix_types", (tuple, list))
|
|
self.__check_attr("multi_dataset__matrix_cache__max_datasets", int)
|
|
self.__check_attr("multi_dataset__matrix_cache__timelimit_s", (type(None), int, float))
|
|
|
|
if self.multi_dataset__dataroot is None:
|
|
return
|
|
|
|
# error checking
|
|
for mtype in self.multi_dataset__allowed_matrix_types:
|
|
try:
|
|
MatrixDataType(mtype)
|
|
except ValueError:
|
|
raise ConfigurationError(f'Invalid matrix type in "allowed_matrix_types": {mtype}')
|
|
|
|
# create the matrix data cache manager:
|
|
if self.matrix_data_cache_manager is None:
|
|
self.matrix_data_cache_manager = MatrixDataCacheManager(
|
|
max_cached=self.multi_dataset__matrix_cache__max_datasets,
|
|
timelimit_s=self.multi_dataset__matrix_cache__timelimit_s,
|
|
)
|
|
|
|
def handle_user_annotations(self, context):
|
|
self.__check_attr("user_annotations__enable", bool)
|
|
self.__check_attr("user_annotations__type", str)
|
|
self.__check_attr("user_annotations__local_file_csv__directory", (type(None), str))
|
|
self.__check_attr("user_annotations__local_file_csv__file", (type(None), str))
|
|
self.__check_attr("user_annotations__ontology__enable", bool)
|
|
self.__check_attr("user_annotations__ontology__obo_location", (type(None), str))
|
|
|
|
if self.user_annotations__enable:
|
|
# TODO, replace this with a factory pattern once we have more than one way
|
|
# to do annotations. currently only local_file_csv
|
|
if self.user_annotations__type != "local_file_csv":
|
|
raise ConfigurationError('The only annotation type support is "local_file_csv"')
|
|
|
|
dirname = self.user_annotations__local_file_csv__directory
|
|
filename = self.user_annotations__local_file_csv__file
|
|
|
|
if filename is not None and dirname is not None:
|
|
raise ConfigurationError("'annotations-file' and 'annotations-dir' may not be used together.")
|
|
|
|
if filename is not None:
|
|
lf_name, lf_ext = splitext(filename)
|
|
if lf_ext and lf_ext != ".csv":
|
|
raise ConfigurationError(f"annotation file type must be .csv: {filename}")
|
|
|
|
if dirname is not None and not isdir(dirname):
|
|
try:
|
|
os.mkdir(dirname)
|
|
except OSError:
|
|
raise ConfigurationError("Unable to create directory specified by --annotations-dir")
|
|
|
|
self.user_annotations = AnnotationsLocalFile(dirname, filename)
|
|
|
|
# if the user has specified a fixed label file, go ahead and validate it
|
|
# so that we can remove errors early in the process.
|
|
if self.single_dataset__datapath and self.user_annotations__local_file_csv__file:
|
|
with self.matrix_data_cache_manager.data_adaptor(self.single_dataset__datapath, self) as data_adaptor:
|
|
data_adaptor.check_new_labels(self.user_annotations.read_labels(data_adaptor))
|
|
|
|
if self.user_annotations__ontology__enable or self.user_annotations__ontology__obo_location:
|
|
try:
|
|
self.user_annotations.load_ontology(self.user_annotations__ontology__obo_location)
|
|
except OntologyLoadFailure as e:
|
|
raise ConfigurationError("Unable to load ontology terms\n" + str(e))
|
|
|
|
else:
|
|
if self.user_annotations__type == "local_file_csv":
|
|
dirname = self.user_annotations__local_file_csv__directory
|
|
filename = self.user_annotations__local_file_csv__file
|
|
if filename is not None:
|
|
context["messsagefn"]("Warning: --annotations-file ignored as annotations are disabled.")
|
|
if dirname is not None:
|
|
context["messagefn"]("Warning: --annotations-dir ignored as annotations are disabled.")
|
|
|
|
if self.user_annotations__ontology__enable:
|
|
context["messagefn"](
|
|
"Warning: --experimental-annotations-ontology" " ignored as annotations are disabled."
|
|
)
|
|
if self.user_annotations__ontology__obo_location is not None:
|
|
context["messagefn"](
|
|
"Warning: --experimental-annotations-ontology-obo" " ignored as annotations are disabled."
|
|
)
|
|
|
|
def handle_embeddings(self, context):
|
|
self.__check_attr("embeddings__names", (list, tuple))
|
|
self.__check_attr("embeddings__enable_reembedding", bool)
|
|
|
|
if self.single_dataset__datapath:
|
|
if self.embeddings__enable_reembedding:
|
|
matrix_data_loader = MatrixDataLoader(self.single_dataset__datapath, app_config=self)
|
|
if matrix_data_loader.matrix_data_type() != MatrixDataType.H5AD:
|
|
raise ConfigurationError("'enable-reembedding is only supported with H5AD files.")
|
|
if self.adaptor__anndata_adaptor__backed:
|
|
raise ConfigurationError("enable-reembedding is not supported when run in --backed mode.")
|
|
|
|
def handle_diffexp(self, context):
|
|
self.__check_attr("diffexp__enable", bool)
|
|
self.__check_attr("diffexp__lfc_cutoff", float)
|
|
self.__check_attr("diffexp__top_n", int)
|
|
self.__check_attr("diffexp__alg_cxg__max_workers", (str, int))
|
|
self.__check_attr("diffexp__alg_cxg__cpu_multiplier", int)
|
|
self.__check_attr("diffexp__alg_cxg__target_workunit", int)
|
|
|
|
if self.single_dataset__datapath:
|
|
with self.matrix_data_cache_manager.data_adaptor(self.single_dataset__datapath, self) as data_adaptor:
|
|
if self.diffexp__enable and data_adaptor.parameters.get("diffexp_may_be_slow", False):
|
|
context["messagefn"](
|
|
f"CAUTION: due to the size of your dataset, "
|
|
f"running differential expression may take longer or fail."
|
|
)
|
|
|
|
max_workers = self.diffexp__alg_cxg__max_workers
|
|
cpu_multiplier = self.diffexp__alg_cxg__cpu_multiplier
|
|
cpu_count = os.cpu_count()
|
|
max_workers = min(max_workers, cpu_multiplier * cpu_count)
|
|
diffexp_tiledb.set_config(
|
|
max_workers,
|
|
self.diffexp__alg_cxg__target_workunit)
|
|
|
|
def handle_adaptor(self, context):
|
|
# cxg
|
|
self.__check_attr("adaptor__cxg_adaptor__tiledb_ctx", dict)
|
|
regionkey = "vfs.s3.region"
|
|
if regionkey not in self.adaptor__cxg_adaptor__tiledb_ctx:
|
|
if type(self.data_locator__s3__region_name) == str:
|
|
self.adaptor__cxg_adaptor__tiledb_ctx[regionkey] = self.data_locator__s3__region_name
|
|
|
|
from server.data_cxg.cxg_adaptor import CxgAdaptor
|
|
|
|
CxgAdaptor.set_tiledb_context(self.adaptor__cxg_adaptor__tiledb_ctx)
|
|
|
|
# anndata
|
|
self.__check_attr("adaptor__anndata_adaptor__backed", bool)
|
|
|
|
def handle_limits(self, context):
|
|
self.__check_attr("limits__diffexp_cellcount_max", (type(None), int))
|
|
self.__check_attr("limits__column_request_max", (type(None), int))
|
|
|
|
def get_title(self, data_adaptor):
|
|
return self.single_dataset__title if self.single_dataset__title else data_adaptor.get_title()
|
|
|
|
def get_about(self, data_adaptor):
|
|
return self.single_dataset__about if self.single_dataset__about else data_adaptor.get_about()
|
|
|
|
def get_client_config(self, data_adaptor, annotation=None):
|
|
"""
|
|
Return the configuration as required by the /config REST route
|
|
"""
|
|
|
|
# FIXME The current set of config is not consistently presented:
|
|
# we have camalCase, hyphen-text, and underscore_text
|
|
|
|
# make sure the configuration has been checked.
|
|
self.check_config()
|
|
|
|
# features
|
|
features = [f.todict() for f in data_adaptor.get_features(annotation)]
|
|
|
|
# display_names
|
|
title = self.get_title(data_adaptor)
|
|
about = self.get_about(data_adaptor)
|
|
|
|
display_names = dict(engine=data_adaptor.get_name(), dataset=title)
|
|
|
|
# library_versions
|
|
library_versions = {}
|
|
library_versions.update(data_adaptor.get_library_versions())
|
|
library_versions["cellxgene"] = cellxgene_version
|
|
|
|
# links
|
|
links = {"about-dataset": about}
|
|
|
|
# parameters
|
|
parameters = {
|
|
"layout": self.embeddings__names,
|
|
"max-category-items": self.presentation__max_categories,
|
|
"obs_names": self.single_dataset__obs_names,
|
|
"var_names": self.single_dataset__var_names,
|
|
"diffexp_lfc_cutoff": self.diffexp__lfc_cutoff,
|
|
"backed": self.adaptor__anndata_adaptor__backed,
|
|
"disable-diffexp": not self.diffexp__enable,
|
|
"enable-reembedding": self.embeddings__enable_reembedding,
|
|
"annotations": False,
|
|
"annotations_file": None,
|
|
"annotations_dir": None,
|
|
"annotations_cell_ontology_enabled": False,
|
|
"annotations_cell_ontology_obopath": None,
|
|
"annotations_cell_ontology_terms": None,
|
|
"diffexp-may-be-slow": False,
|
|
"about_legal_tos": self.server__about_legal_tos,
|
|
"about_legal_privacy": self.server__about_legal_privacy,
|
|
}
|
|
|
|
data_adaptor.update_parameters(parameters)
|
|
if annotation:
|
|
annotation.update_parameters(parameters, data_adaptor)
|
|
|
|
# gather it all together
|
|
c = {}
|
|
config = c["config"] = {}
|
|
config["features"] = features
|
|
config["displayNames"] = display_names
|
|
config["library_versions"] = library_versions
|
|
config["links"] = links
|
|
config["parameters"] = parameters
|
|
config["limits"] = {
|
|
'column_request_max': self.limits__column_request_max,
|
|
'diffexp_cellcount_max': self.limits__diffexp_cellcount_max,
|
|
}
|
|
|
|
return c
|
|
|
|
def exceeds_limit(self, limit_name, value):
|
|
limit_value = getattr(self, "limits__" + limit_name, None)
|
|
if limit_value is None: # disabled
|
|
return False
|
|
return value > limit_value
|