mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-16 21:37:59 +08:00
* split out config * add tests for base and app config, refactor client config out of app config * refactor default config retrieval * create config test class and helper functions * move default_config into server to fix import issue
179 lines
8.1 KiB
Python
179 lines
8.1 KiB
Python
import json
|
|
|
|
import numpy as np
|
|
import tiledb
|
|
|
|
from server.common.utils.type_conversion_utils import get_dtype_of_array, get_dtype_and_schema_of_array
|
|
|
|
|
|
def convert_dictionary_to_cxg_group(cxg_container, metadata_dict, group_metadata_name="cxg_group_metadata"):
|
|
"""
|
|
Saves the contents of the dictionary to the CXG output directory specified.
|
|
|
|
This function is primarily used to save metadata about a dataset to the CXG directory. At some point, tiledb will
|
|
have support for metadata on groups at which point the utility of this function should be revisited. Until such
|
|
feature exists, this function create an empty array and annotate that array.
|
|
|
|
For more information, visit https://github.com/TileDB-Inc/TileDB-Py/issues/254.
|
|
"""
|
|
|
|
array_name = f"{cxg_container}/{group_metadata_name}"
|
|
|
|
# Because TileDB does not allow one to attach metadata directly to a CXG group, we need to have a workaround
|
|
# where we create an empty array and attached the metadata onto to this empty array. Below we construct this empty
|
|
# array.
|
|
tiledb.from_numpy(array_name, np.zeros((1,)))
|
|
|
|
with tiledb.DenseArray(array_name, mode="w") as metadata_array:
|
|
for key, value in metadata_dict.items():
|
|
metadata_array.meta[key] = value
|
|
|
|
|
|
def convert_dataframe_to_cxg_array(cxg_container, dataframe_name, dataframe, index_column_name, ctx):
|
|
"""
|
|
Saves the contents of the dataframe to the CXG output directory specified.
|
|
|
|
Current access patterns are oriented toward reading very large slices of the dataframe, one attribute at a time.
|
|
Attribute data also tends to be (often) repetitive (bools, categories, strings). Given this, we use a large tile
|
|
size (1000) and very aggressive compression levels.
|
|
"""
|
|
|
|
def create_dataframe_array(array_name, dataframe):
|
|
tiledb_filter = tiledb.FilterList(
|
|
[
|
|
# Attempt aggressive compression as many of these dataframes are very repetitive strings, bools and
|
|
# other non-float data.
|
|
tiledb.ZstdFilter(level=22),
|
|
]
|
|
)
|
|
attrs = [
|
|
tiledb.Attr(name=column, dtype=get_dtype_of_array(dataframe[column]), filters=tiledb_filter)
|
|
for column in dataframe
|
|
]
|
|
domain = tiledb.Domain(
|
|
tiledb.Dim(domain=(0, dataframe.shape[0] - 1), tile=min(dataframe.shape[0], 1000), dtype=np.uint32)
|
|
)
|
|
schema = tiledb.ArraySchema(
|
|
domain=domain, sparse=False, attrs=attrs, cell_order="row-major", tile_order="row-major"
|
|
)
|
|
tiledb.DenseArray.create(array_name, schema)
|
|
|
|
array_name = f"{cxg_container}/{dataframe_name}"
|
|
|
|
create_dataframe_array(array_name, dataframe)
|
|
|
|
with tiledb.DenseArray(array_name, mode="w", ctx=ctx) as array:
|
|
value = {}
|
|
schema_hints = {}
|
|
for column_name, column_values in dataframe.items():
|
|
dtype, hints = get_dtype_and_schema_of_array(column_values)
|
|
value[column_name] = column_values.to_numpy(dtype=dtype)
|
|
if hints:
|
|
schema_hints.update({column_name: hints})
|
|
|
|
schema_hints.update({"index": index_column_name})
|
|
array[:] = value
|
|
array.meta["cxg_schema"] = json.dumps(schema_hints)
|
|
|
|
tiledb.consolidate(array_name, ctx=ctx)
|
|
|
|
|
|
def convert_ndarray_to_cxg_dense_array(ndarray_name, ndarray, ctx):
|
|
"""
|
|
Saves contents of ndarray to the CXG output directory specified.
|
|
|
|
Generally this function is used to convert dataset embeddings. Because embeddings are typically accessed with
|
|
very large slices (or all of the embedding), they do not benefit from overly aggressive compression due to their
|
|
format. Given this, we use a large tile size (1000) but only default compression level.
|
|
"""
|
|
|
|
def create_ndarray_array(ndarray_name, ndarray):
|
|
filters = tiledb.FilterList([tiledb.ZstdFilter()])
|
|
attrs = [tiledb.Attr(dtype=ndarray.dtype, filters=filters)]
|
|
dimensions = [
|
|
tiledb.Dim(
|
|
domain=(0, ndarray.shape[dimension] - 1), tile=min(ndarray.shape[dimension], 1000), dtype=np.uint32
|
|
)
|
|
for dimension in range(ndarray.ndim)
|
|
]
|
|
domain = tiledb.Domain(*dimensions)
|
|
schema = tiledb.ArraySchema(
|
|
domain=domain, sparse=False, attrs=attrs, capacity=1_000_000, cell_order="row-major", tile_order="row-major"
|
|
)
|
|
tiledb.DenseArray.create(ndarray_name, schema)
|
|
|
|
create_ndarray_array(ndarray_name, ndarray)
|
|
|
|
with tiledb.DenseArray(ndarray_name, mode="w", ctx=ctx) as array:
|
|
array[:] = ndarray
|
|
|
|
tiledb.consolidate(ndarray_name, ctx=ctx)
|
|
|
|
|
|
def convert_matrix_to_cxg_array(
|
|
matrix_name, matrix, encode_as_sparse_array, ctx, column_shift_for_sparse_encoding=None
|
|
):
|
|
"""
|
|
Converts a numpy array matrix into a TileDB SparseArray of DenseArray based on whether `encode_as_sparse_array`
|
|
is true or not. Note that when the matrix is encoded as a SparseArray, it only writes the values that are
|
|
nonzero. This means that if you count the number of elements in the SparseArray, it will not equal the total
|
|
number of elements in the matrix, only the number of nonzero elements.
|
|
|
|
Furthermore, if the `column_shift_for_sparse_encoding` matrix is not None, this function will subtract the sparse
|
|
encoding from the original given matrix and as previously stated, only write the nonzero values to the TileDB
|
|
SparseArray.
|
|
"""
|
|
|
|
def create_matrix_array(matrix_name, number_of_rows, number_of_columns, encode_as_sparse_array):
|
|
filters = tiledb.FilterList([tiledb.ZstdFilter()])
|
|
attrs = [tiledb.Attr(dtype=np.float32, filters=filters)]
|
|
if encode_as_sparse_array:
|
|
domain = tiledb.Domain(
|
|
tiledb.Dim(name="obs", domain=(0, number_of_rows - 1), tile=min(number_of_rows, 512), dtype=np.uint32),
|
|
tiledb.Dim(
|
|
name="var", domain=(0, number_of_columns - 1), tile=min(number_of_columns, 2048), dtype=np.uint32
|
|
),
|
|
)
|
|
else:
|
|
domain = tiledb.Domain(
|
|
tiledb.Dim(name="obs", domain=(0, number_of_rows - 1), tile=min(number_of_rows, 50), dtype=np.uint32),
|
|
tiledb.Dim(
|
|
name="var", domain=(0, number_of_columns - 1), tile=min(number_of_columns, 100), dtype=np.uint32
|
|
),
|
|
)
|
|
schema = tiledb.ArraySchema(
|
|
domain=domain, sparse=encode_as_sparse_array, attrs=attrs, cell_order="row-major", tile_order="col-major"
|
|
)
|
|
if encode_as_sparse_array:
|
|
tiledb.SparseArray.create(matrix_name, schema)
|
|
else:
|
|
tiledb.DenseArray.create(matrix_name, schema)
|
|
|
|
number_of_rows = matrix.shape[0]
|
|
number_of_columns = matrix.shape[1]
|
|
stride = min(int(np.power(10, np.around(np.log10(1e9 / number_of_columns)))), 10_000)
|
|
|
|
create_matrix_array(matrix_name, number_of_rows, number_of_columns, encode_as_sparse_array)
|
|
|
|
if encode_as_sparse_array:
|
|
with tiledb.SparseArray(matrix_name, mode="w", ctx=ctx) as array:
|
|
for start_row_index in range(0, number_of_rows, stride):
|
|
end_row_index = min(start_row_index + stride, number_of_rows)
|
|
matrix_subset = matrix[start_row_index:end_row_index, :]
|
|
if not isinstance(matrix_subset, np.ndarray):
|
|
matrix_subset = matrix_subset.toarray()
|
|
if column_shift_for_sparse_encoding is not None:
|
|
matrix_subset = matrix_subset - column_shift_for_sparse_encoding
|
|
indices = np.nonzero(matrix_subset)
|
|
trow = indices[0] + start_row_index
|
|
array[trow, indices[1]] = matrix_subset[indices[0], indices[1]]
|
|
|
|
else:
|
|
with tiledb.DenseArray(matrix_name, mode="w", ctx=ctx) as array:
|
|
for start_row_index in range(0, number_of_rows, stride):
|
|
end_row_index = min(start_row_index + stride, number_of_rows)
|
|
matrix_subset = matrix[start_row_index:end_row_index, :]
|
|
if not isinstance(matrix_subset, np.ndarray):
|
|
matrix_subset = matrix_subset.toarray()
|
|
array[start_row_index:end_row_index, :] = matrix_subset
|