mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-19 02:48:30 +08:00
This PR contains a refactoring to make adding new features easier. The new features include supporting the tiledb format, and the multi dataset application. The refactoring includes Simplifying the directory structure and files. a class structure to handle annotations (currently one type: AnnotationsLocalFile). a class to handle application configuration a class structure to handle matrix data (currently AnndataAdaptor and CxgAdaptor). CxgAdaptor uses tiledb. Algorithms that were previously dependent on the scanpy anndata object are now generalized to work with an abstract interface. The multi dataset option is not fully supported yet, and so the option to use it is hidden. Use "cli launch --dataroot ..." To access this feature. All combinations of app single dataset/ app multi dataset and AnndataAdaptor/CxgAdaptor work with all the features, such as annotations, ontologies, diffexp.
147 lines
4.3 KiB
Python
147 lines
4.3 KiB
Python
import contextlib
|
|
import errno
|
|
import socket
|
|
from urllib.parse import urlsplit, urljoin
|
|
import os
|
|
from flask import json
|
|
import numpy as np
|
|
import pandas as pd
|
|
import warnings
|
|
|
|
|
|
def find_available_port(host, port=5005):
|
|
"""
|
|
Helper method to find open port on host. Tries 5000 ports incremented from the specified port
|
|
"""
|
|
# Takes approx 2 seconds to do a scan of 5000 ports on my laptop
|
|
num_ports_to_try = 5000
|
|
for port_to_try in range(port, port + num_ports_to_try):
|
|
if is_port_available(host, port_to_try):
|
|
return port_to_try
|
|
raise socket.error(errno.EADDRINUSE, f"No port in range {port} - {port + num_ports_to_try - 1} available.")
|
|
|
|
|
|
def is_port_available(host, port):
|
|
is_available = False
|
|
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
|
try:
|
|
s.bind((host, port))
|
|
is_available = True
|
|
except socket.error:
|
|
pass
|
|
return is_available
|
|
|
|
|
|
def sort_options(command):
|
|
"""
|
|
Helper for the click options - will sort options in a command, and can
|
|
be used as a decorator.
|
|
"""
|
|
command.params.sort(key=lambda p: p.name)
|
|
return command
|
|
|
|
|
|
def path_join(base, *urls):
|
|
"""
|
|
this is like urllib.parse.urljoin, except it works around the scheme-specific
|
|
cleverness in the aforementioned code, ignores anything in the url except the path,
|
|
and accepts more than one url.
|
|
"""
|
|
if not base.endswith("/"):
|
|
base += "/"
|
|
btpl = urlsplit(base)
|
|
path = btpl.path
|
|
for url in urls:
|
|
utpl = urlsplit(url)
|
|
if btpl.scheme == "":
|
|
path = os.path.join(path, utpl.path)
|
|
path = os.path.normpath(path)
|
|
else:
|
|
path = urljoin(path, utpl.path)
|
|
return btpl._replace(path=path).geturl()
|
|
|
|
|
|
class Float32JSONEncoder(json.JSONEncoder):
|
|
def __init__(self, *args, **kwargs):
|
|
"""
|
|
NaN/Infinities are illegal in standard JSON. Python extends JSON with
|
|
non-standard symbols that most JavaScript JSON parsers do not understand.
|
|
The `allow_nan` parameter will force Python simplejson to throw an ValueError
|
|
if it runs into non-finite floating point values which are unsupported by
|
|
standard JSON.
|
|
"""
|
|
kwargs["allow_nan"] = False
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def default(self, obj):
|
|
if isinstance(obj, np.float32):
|
|
return float(obj)
|
|
elif isinstance(obj, np.integer):
|
|
return int(obj)
|
|
return json.JSONEncoder.default(self, obj)
|
|
|
|
|
|
def custom_format_warning(msg, *args, **kwargs):
|
|
return f"[cellxgene] Warning: {msg} \n"
|
|
|
|
|
|
def jsonify_numpy(data):
|
|
return json.dumps(data, cls=Float32JSONEncoder, allow_nan=False)
|
|
|
|
|
|
def dtype_to_schema(dtype):
|
|
schema = {}
|
|
if dtype == np.float32:
|
|
schema['type'] = 'float32'
|
|
elif dtype == np.int32:
|
|
schema['type'] = 'int32'
|
|
elif dtype == np.bool_:
|
|
schema['type'] = 'boolean'
|
|
elif dtype == np.str:
|
|
schema['type'] = 'string'
|
|
elif dtype == "category":
|
|
schema["type"] = "categorical"
|
|
schema["categories"] = dtype.categories.tolist()
|
|
else:
|
|
raise TypeError(
|
|
f"Annotations of type {dtype} are unsupported."
|
|
)
|
|
return schema
|
|
|
|
|
|
def can_cast_to_float32(array):
|
|
if array.dtype.kind == "f":
|
|
if not np.can_cast(array.dtype, np.float32):
|
|
warnings.warn(f"Annotation {array.name} will be converted to 32 bit float and may lose precision.")
|
|
return True
|
|
return False
|
|
|
|
|
|
def can_cast_to_int32(array):
|
|
if array.dtype.kind in ["i", "u"]:
|
|
if np.can_cast(array.dtype, np.int32):
|
|
return True
|
|
ii32 = np.iinfo(np.int32)
|
|
if array.min() >= ii32.min and array.max() <= ii32.max:
|
|
return True
|
|
return False
|
|
|
|
|
|
def series_to_schema(array):
|
|
assert type(array) == pd.Series
|
|
try:
|
|
return dtype_to_schema(array.dtype)
|
|
except TypeError:
|
|
dtype = array.dtype
|
|
data_kind = dtype.kind
|
|
schema = {}
|
|
if can_cast_to_float32(array):
|
|
schema["type"] = "float32"
|
|
elif can_cast_to_int32(array):
|
|
schema["type"] = "int32"
|
|
elif data_kind == "O" and dtype == "object":
|
|
schema["type"] = "string"
|
|
else:
|
|
raise TypeError(f"Annotations of type {dtype} are unsupported.")
|
|
return schema
|