mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-06 13:08:11 +08:00
add multi-layout support to back-end (#766)
* add multi-layout support to back-end * remove obsolete code * temporary code to apply heuristic choice of default layout * fix tests * update python tests * more py lint * PR review changes * more PR lint * PR lint
This commit is contained in:
@@ -162,29 +162,7 @@ const aLayoutFBSResponse = (() => {
|
|||||||
new Float32Array(nObs).fill(Math.random()),
|
new Float32Array(nObs).fill(Math.random()),
|
||||||
new Float32Array(nObs).fill(Math.random())
|
new Float32Array(nObs).fill(Math.random())
|
||||||
];
|
];
|
||||||
const builder = new flatbuffers.Builder(1024);
|
return encodeMatrix(coords, ["umap_0", "umap_1"]);
|
||||||
|
|
||||||
const cols = _.map(coords, carr => {
|
|
||||||
const cdv = NetEncoding.Float32Array.createDataVector(builder, carr);
|
|
||||||
NetEncoding.Float32Array.startFloat32Array(builder);
|
|
||||||
NetEncoding.Float32Array.addData(builder, cdv);
|
|
||||||
const floatArr = NetEncoding.Float32Array.endFloat32Array(builder);
|
|
||||||
|
|
||||||
NetEncoding.Column.startColumn(builder);
|
|
||||||
NetEncoding.Column.addUType(builder, NetEncoding.TypedArray.Float32Array);
|
|
||||||
NetEncoding.Column.addU(builder, floatArr);
|
|
||||||
return NetEncoding.Column.endColumn(builder);
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns = NetEncoding.Matrix.createColumnsVector(builder, cols);
|
|
||||||
|
|
||||||
NetEncoding.Matrix.startMatrix(builder);
|
|
||||||
NetEncoding.Matrix.addNRows(builder, nObs);
|
|
||||||
NetEncoding.Matrix.addNCols(builder, coords.length);
|
|
||||||
NetEncoding.Matrix.addColumns(builder, columns);
|
|
||||||
const matrix = NetEncoding.Matrix.endMatrix(builder);
|
|
||||||
builder.finish(matrix);
|
|
||||||
return builder.asUint8Array();
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const aDataObsResponse = {
|
const aDataObsResponse = {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ function AnnotationsFBSToDataframe(arrayBuffer) {
|
|||||||
The application has strong assumptions that all scalar data will be
|
The application has strong assumptions that all scalar data will be
|
||||||
stored as a float32 or float64 (regardless of underlying data types).
|
stored as a float32 or float64 (regardless of underlying data types).
|
||||||
For example, clipping of value ranges (eg, user-selected percentiles)
|
For example, clipping of value ranges (eg, user-selected percentiles)
|
||||||
|
depends on the ability to use NaN in any numeric type.
|
||||||
|
|
||||||
All float data from the server is left as is. All non-float is promoted
|
All float data from the server is left as is. All non-float is promoted
|
||||||
to an appropriate float.
|
to an appropriate float.
|
||||||
@@ -98,13 +99,30 @@ function AnnotationsFBSToDataframe(arrayBuffer) {
|
|||||||
|
|
||||||
function LayoutFBSToDataframe(arrayBuffer) {
|
function LayoutFBSToDataframe(arrayBuffer) {
|
||||||
const fbs = decodeMatrixFBS(arrayBuffer, true);
|
const fbs = decodeMatrixFBS(arrayBuffer, true);
|
||||||
if (fbs.columns.length !== 2 || !fbs.columns.every(isFpTypedArray)) {
|
if (fbs.columns.length < 2 || !fbs.columns.every(isFpTypedArray)) {
|
||||||
// We have strong assumptions about the shape & type of layout data.
|
// We have strong assumptions about the shape & type of layout data.
|
||||||
throw new Error("Unexpected layout data type returned from server");
|
throw new Error("Unexpected layout data type returned from server");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: XXX
|
||||||
|
|
||||||
|
TEMPORARY CODE AND COMMENT to support the progressive implementation
|
||||||
|
of multi-layout support. For now, we search for one of the following
|
||||||
|
in the layouts and use it if we find it: umap, then tsne, then pca,
|
||||||
|
then whatever is first in the list.
|
||||||
|
*/
|
||||||
|
let layoutIndex = 0;
|
||||||
|
["umap", "tsne", "pca"].some(name => {
|
||||||
|
const idx = fbs.colIdx.indexOf(`${name}_0`);
|
||||||
|
if (idx !== -1) {
|
||||||
|
layoutIndex = idx;
|
||||||
|
}
|
||||||
|
return idx !== -1;
|
||||||
|
});
|
||||||
const df = new Dataframe.Dataframe(
|
const df = new Dataframe.Dataframe(
|
||||||
[fbs.nRows, fbs.nCols],
|
[fbs.nRows, 2],
|
||||||
fbs.columns,
|
[fbs.columns[layoutIndex], fbs.columns[layoutIndex + 1]],
|
||||||
null,
|
null,
|
||||||
new Dataframe.KeyIndex(["X", "Y"])
|
new Dataframe.KeyIndex(["X", "Y"])
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ Currently this is not supported directly, but you should be able to do this your
|
|||||||
|
|
||||||
- `.obs` and `.var` annotations are use to extract metadata for filtering
|
- `.obs` and `.var` annotations are use to extract metadata for filtering
|
||||||
- `.X` is used to display expression (histograms, scatterplot & colorscale) and to compute differential expression
|
- `.X` is used to display expression (histograms, scatterplot & colorscale) and to compute differential expression
|
||||||
- `.obsm` is used for layout
|
- `.obsm` is used for layout. If an embedding has more than two components, the first two will be used for visualization.
|
||||||
|
|
||||||
#### I have a BIG dataset - how can I make cellxgene run as fast as possible?
|
#### I have a BIG dataset - how can I make cellxgene run as fast as possible?
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas
|
||||||
from pandas.core.dtypes.dtypes import CategoricalDtype
|
from pandas.core.dtypes.dtypes import CategoricalDtype
|
||||||
import anndata
|
import anndata
|
||||||
from scipy import sparse
|
from scipy import sparse
|
||||||
|
|
||||||
from server.app.driver.driver import CXGDriver
|
from server.app.driver.driver import CXGDriver
|
||||||
from server.app.util.constants import Axis, DEFAULT_TOP_N
|
from server.app.util.constants import Axis, DEFAULT_TOP_N, MAX_LAYOUTS
|
||||||
from server.app.util.errors import (
|
from server.app.util.errors import (
|
||||||
FilterError,
|
FilterError,
|
||||||
JSONEncodingValueError,
|
JSONEncodingValueError,
|
||||||
@@ -41,7 +42,7 @@ class ScanpyEngine(CXGDriver):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_default_config():
|
def _get_default_config():
|
||||||
return {
|
return {
|
||||||
"layout": "umap",
|
"layout": [],
|
||||||
"diffexp": "ttest",
|
"diffexp": "ttest",
|
||||||
"max_category_items": 100,
|
"max_category_items": 100,
|
||||||
"obs_names": None,
|
"obs_names": None,
|
||||||
@@ -166,11 +167,55 @@ class ScanpyEngine(CXGDriver):
|
|||||||
self._alias_annotation_names(Axis.OBS, self.config["obs_names"])
|
self._alias_annotation_names(Axis.OBS, self.config["obs_names"])
|
||||||
self._alias_annotation_names(Axis.VAR, self.config["var_names"])
|
self._alias_annotation_names(Axis.VAR, self.config["var_names"])
|
||||||
self._validate_data_types()
|
self._validate_data_types()
|
||||||
self._validate_data_calculations()
|
|
||||||
self.cell_count = self.data.shape[0]
|
self.cell_count = self.data.shape[0]
|
||||||
self.gene_count = self.data.shape[1]
|
self.gene_count = self.data.shape[1]
|
||||||
|
self._default_and_validate_layouts()
|
||||||
self._create_schema()
|
self._create_schema()
|
||||||
|
|
||||||
|
@requires_data
|
||||||
|
def _default_and_validate_layouts(self):
|
||||||
|
""" function:
|
||||||
|
a) generate list of default layouts, if not already user specified
|
||||||
|
b) validate layouts are legal. remove/warn on any that are not
|
||||||
|
c) cap total list of layouts at global const MAX_LAYOUTS
|
||||||
|
"""
|
||||||
|
layouts = self.config['layout']
|
||||||
|
# handle default
|
||||||
|
if layouts is None or len(layouts) == 0:
|
||||||
|
# load default layouts from the data.
|
||||||
|
layouts = [key[2:] for key in self.data.obsm_keys() if type(key) == str and key.startswith("X_")]
|
||||||
|
if len(layouts) == 0:
|
||||||
|
raise PrepareError(f"Unable to find any precomputed layouts within the dataset.")
|
||||||
|
|
||||||
|
# remove invalid layouts
|
||||||
|
valid_layouts = []
|
||||||
|
obsm_keys = self.data.obsm_keys()
|
||||||
|
for layout in layouts:
|
||||||
|
layout_name = f"X_{layout}"
|
||||||
|
if layout_name not in obsm_keys:
|
||||||
|
warnings.warn(f"Ignoring unknown layout name: {layout}.")
|
||||||
|
elif not self._is_valid_layout(self.data.obsm[layout_name]):
|
||||||
|
warnings.warn(f"Ignoring layout due to malformed shape or data type: {layout}")
|
||||||
|
else:
|
||||||
|
valid_layouts.append(layout)
|
||||||
|
|
||||||
|
if len(valid_layouts) == 0:
|
||||||
|
raise PrepareError(f"No valid layout data.")
|
||||||
|
|
||||||
|
# cap layouts to MAX_LAYOUTS
|
||||||
|
self.config['layout'] = valid_layouts[0:MAX_LAYOUTS]
|
||||||
|
|
||||||
|
@requires_data
|
||||||
|
def _is_valid_layout(self, arr):
|
||||||
|
""" return True if this layout data is a valid array for front-end presentation:
|
||||||
|
* ndarray, with shape (n_obs, >= 2), dtype float/int/uint
|
||||||
|
* contains only finite values
|
||||||
|
"""
|
||||||
|
is_valid = type(arr) == np.ndarray and arr.dtype.kind in "fiu"
|
||||||
|
is_valid = is_valid and arr.shape[0] == self.data.n_obs and arr.shape[1] >= 2
|
||||||
|
is_valid = is_valid and np.all(np.isfinite(arr))
|
||||||
|
return is_valid
|
||||||
|
|
||||||
@requires_data
|
@requires_data
|
||||||
def _validate_data_types(self):
|
def _validate_data_types(self):
|
||||||
if sparse.isspmatrix(self.data.X) and not sparse.isspmatrix_csc(self.data.X):
|
if sparse.isspmatrix(self.data.X) and not sparse.isspmatrix_csc(self.data.X):
|
||||||
@@ -208,20 +253,6 @@ class ScanpyEngine(CXGDriver):
|
|||||||
f"annotations with more than 500 categories in the UI"
|
f"annotations with more than 500 categories in the UI"
|
||||||
)
|
)
|
||||||
|
|
||||||
@requires_data
|
|
||||||
def _validate_data_calculations(self):
|
|
||||||
layout_key = f"X_{self.config['layout']}"
|
|
||||||
try:
|
|
||||||
assert layout_key in self.data.obsm_keys()
|
|
||||||
except AssertionError:
|
|
||||||
raise PrepareError(
|
|
||||||
f"Cannot find a field with coordinates for the {self.config['layout']} layout requested. A different"
|
|
||||||
f" layout may have been computed. The requested layout must be pre-calculated and saved "
|
|
||||||
f"back in the h5ad file. You can run "
|
|
||||||
f"`cellxgene prepare --layout {self.config['layout']} <datafile>` "
|
|
||||||
f"to solve this problem. "
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _annotation_filter_to_mask(filter, d_axis, count):
|
def _annotation_filter_to_mask(filter, d_axis, count):
|
||||||
mask = np.ones((count,), dtype=bool)
|
mask = np.ones((count,), dtype=bool)
|
||||||
@@ -372,15 +403,18 @@ class ScanpyEngine(CXGDriver):
|
|||||||
* only returns Matrix in columnar layout
|
* only returns Matrix in columnar layout
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
full_embedding = self.data.obsm[f"X_{self.config['layout']}"]
|
layout_data = []
|
||||||
if full_embedding.shape[1] > 2:
|
for layout in self.config["layout"]:
|
||||||
warnings.warn(f"Warning: found {full_embedding.shape[1]} \
|
full_embedding = self.data.obsm[f"X_{layout}"]
|
||||||
components of embedding. Using the first two for layout display.")
|
embedding = full_embedding[:, :2]
|
||||||
df_layout = full_embedding[:, :2]
|
normalized_layout = (embedding - embedding.min()) / (embedding.max() - embedding.min())
|
||||||
|
normalized_layout = normalized_layout.astype(dtype=np.float32)
|
||||||
|
layout_data.append(pandas.DataFrame(normalized_layout, columns=[f"{layout}_0", f"{layout}_1"]))
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise PrepareError(
|
raise PrepareError(
|
||||||
f"Layout has not been calculated using {self.config['layout']}, "
|
f"Layout has not been calculated using {self.config['layout']}, "
|
||||||
f"please prepare your datafile and relaunch cellxgene") from e
|
f"please prepare your datafile and relaunch cellxgene") from e
|
||||||
|
|
||||||
normalized_layout = (df_layout - df_layout.min()) / (df_layout.max() - df_layout.min())
|
df = pandas.concat(layout_data, axis=1, copy=False)
|
||||||
return encode_matrix_fbs(normalized_layout.astype(dtype=np.float32), col_idx=None, row_idx=None)
|
return encode_matrix_fbs(df, col_idx=df.columns, row_idx=None)
|
||||||
|
|||||||
@@ -31,3 +31,5 @@ JSON_NaN_to_num_warning_msg = (
|
|||||||
"JSON encoding failure - please verify all data are finite values (no NaN or Infinities)"
|
"JSON encoding failure - please verify all data are finite values (no NaN or Infinities)"
|
||||||
)
|
)
|
||||||
REACTIVE_LIMIT = 1_000_000
|
REACTIVE_LIMIT = 1_000_000
|
||||||
|
|
||||||
|
MAX_LAYOUTS = 30
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import psutil
|
|||||||
from server.app.app import Server
|
from server.app.app import Server
|
||||||
from server.app.util.errors import ScanpyFileError
|
from server.app.util.errors import ScanpyFileError
|
||||||
from server.app.util.utils import custom_format_warning
|
from server.app.util.utils import custom_format_warning
|
||||||
from server.utils.constants import MODES
|
|
||||||
from server.utils.utils import find_available_port
|
from server.utils.utils import find_available_port
|
||||||
|
|
||||||
|
|
||||||
@@ -25,10 +24,10 @@ BIG_FILE_SIZE_THRESHOLD = 100 * 2**20 # 100MB
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--layout",
|
"--layout",
|
||||||
"-l",
|
"-l",
|
||||||
type=click.Choice(MODES),
|
default=[],
|
||||||
default="umap",
|
multiple=True,
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help="Method for layout."
|
help="Layout name, eg, 'umap'."
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--diffexp",
|
"--diffexp",
|
||||||
|
|||||||
@@ -67,9 +67,11 @@ class EndPoints(unittest.TestCase):
|
|||||||
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)
|
||||||
self.assertEqual(df['n_rows'], 2638)
|
self.assertEqual(df['n_rows'], 2638)
|
||||||
self.assertEqual(df['n_cols'], 2)
|
self.assertEqual(df['n_cols'], 8)
|
||||||
self.assertIsNotNone(df['columns'])
|
self.assertIsNotNone(df['columns'])
|
||||||
self.assertIsNone(df['col_idx'])
|
self.assertListEqual(df['col_idx'], [
|
||||||
|
'pca_0', 'pca_1', 'tsne_0', 'tsne_1', 'umap_0', 'umap_1', 'draw_graph_fr_0', 'draw_graph_fr_1'
|
||||||
|
])
|
||||||
self.assertIsNone(df['row_idx'])
|
self.assertIsNone(df['row_idx'])
|
||||||
self.assertEqual(len(df['columns']), df['n_cols'])
|
self.assertEqual(len(df['columns']), df['n_cols'])
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from server.app.util.errors import FilterError
|
|||||||
class NaNTest(unittest.TestCase):
|
class NaNTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.args = {
|
self.args = {
|
||||||
"layout": "umap",
|
"layout": ["umap"],
|
||||||
"diffexp": "ttest",
|
"diffexp": "ttest",
|
||||||
"max_category_items": 100,
|
"max_category_items": 100,
|
||||||
"obs_names": None,
|
"obs_names": None,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from server.app.util.errors import FilterError
|
|||||||
class EngineTest(unittest.TestCase):
|
class EngineTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
args = {
|
args = {
|
||||||
"layout": "umap",
|
"layout": ["umap"],
|
||||||
"diffexp": "ttest",
|
"diffexp": "ttest",
|
||||||
"max_category_items": 100,
|
"max_category_items": 100,
|
||||||
"obs_names": None,
|
"obs_names": None,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class DataLoadEngineTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_delayed_load_args(self):
|
def test_delayed_load_args(self):
|
||||||
args = {
|
args = {
|
||||||
"layout": "tsne",
|
"layout": ["tsne"],
|
||||||
"diffexp": "ttest",
|
"diffexp": "ttest",
|
||||||
"max_category_items": 1000,
|
"max_category_items": 1000,
|
||||||
"obs_names": "foo",
|
"obs_names": "foo",
|
||||||
|
|||||||
Reference in New Issue
Block a user