From bfbde13ba7b77af36b7ea7de79e341523f985470 Mon Sep 17 00:00:00 2001 From: Trent Smith <1429913+Bento007@users.noreply.github.com> Date: Fri, 28 Jun 2024 08:41:00 -0700 Subject: [PATCH 01/16] [builder] Upgrade to CELLxGENE schema 5.1 (#1192) * feat: upgrade census buidler to schema 5.1 * feat: upgrade COG version * update tests * Apply suggestions from code review * bump census schema version --------- Co-authored-by: Emanuele Bezzi --- tools/cellxgene_census_builder/pyproject.toml | 2 +- .../src/cellxgene_census_builder/build_soma/globals.py | 4 ++-- .../tests/anndata/test_anndata.py | 6 +++--- tools/cellxgene_census_builder/tests/conftest.py | 2 +- tools/cellxgene_census_builder/tests/test_manifest.py | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tools/cellxgene_census_builder/pyproject.toml b/tools/cellxgene_census_builder/pyproject.toml index 5f684c238..4ac3e4d56 100644 --- a/tools/cellxgene_census_builder/pyproject.toml +++ b/tools/cellxgene_census_builder/pyproject.toml @@ -36,7 +36,7 @@ dependencies= [ # https://github.com/TileDB-Inc/TileDB/blob/dev/format_spec/FORMAT_SPEC.md "tiledbsoma==1.9.3", "cellxgene-census==1.12.0", - "cellxgene-ontology-guide==0.6.1", + "cellxgene-ontology-guide==1.0.0", "scipy==1.12.0", "fsspec[http]==2024.3.1", "s3fs==2024.3.1", diff --git a/tools/cellxgene_census_builder/src/cellxgene_census_builder/build_soma/globals.py b/tools/cellxgene_census_builder/src/cellxgene_census_builder/build_soma/globals.py index 7ee72c7a8..ec044a267 100644 --- a/tools/cellxgene_census_builder/src/cellxgene_census_builder/build_soma/globals.py +++ b/tools/cellxgene_census_builder/src/cellxgene_census_builder/build_soma/globals.py @@ -11,9 +11,9 @@ # DataFrame columns. True is enabled, False is disabled. USE_ARROW_DICTIONARY = True -CENSUS_SCHEMA_VERSION = "2.0.1" +CENSUS_SCHEMA_VERSION = "2.1.0" -CXG_SCHEMA_VERSION = "5.0.0" # the CELLxGENE schema version supported +CXG_SCHEMA_VERSION = "5.1.0" # the CELLxGENE schema version supported # Columns expected in the census_datasets dataframe CENSUS_DATASETS_TABLE_SPEC = TableSpec.create( diff --git a/tools/cellxgene_census_builder/tests/anndata/test_anndata.py b/tools/cellxgene_census_builder/tests/anndata/test_anndata.py index 921393c17..dc2f1e999 100644 --- a/tools/cellxgene_census_builder/tests/anndata/test_anndata.py +++ b/tools/cellxgene_census_builder/tests/anndata/test_anndata.py @@ -265,7 +265,7 @@ def test_empty_estimated_density(tmp_path: pathlib.Path) -> None: adata = anndata.AnnData( obs=pd.DataFrame(), var=pd.DataFrame({"feature_id": [0, 1, 2]}), X=sparse.csr_matrix((0, 3), dtype=np.float32) ) - adata.uns["schema_version"] = "5.0.0" + adata.uns["schema_version"] = "5.1.0" adata.write_h5ad(path) with open_anndata(path) as ad: @@ -297,7 +297,7 @@ def test_open_anndata_raw_X(tmp_path: pathlib.Path) -> None: var=pd.DataFrame({"feature_id": [0, 1, 2]}), X=sparse.csr_matrix((2, 3), dtype=np.float32), raw={"X": sparse.csr_matrix((2, 4), dtype=np.float32)}, - uns={"schema_version": "5.0.0"}, + uns={"schema_version": "5.1.0"}, ) adata.write_h5ad(path) @@ -410,7 +410,7 @@ def test_multi_species_filter( index=[f"feature_{i}" for i in range(n_vars)], ), X=sparse.random(n_obs, n_vars, format="csr", dtype=np.float32), - uns={"schema_version": "5.0.0"}, + uns={"schema_version": "5.1.0"}, ) path = (tmp_path / "species.h5ad").as_posix() adata.write_h5ad(path) diff --git a/tools/cellxgene_census_builder/tests/conftest.py b/tools/cellxgene_census_builder/tests/conftest.py index 269860bb2..adccea725 100644 --- a/tools/cellxgene_census_builder/tests/conftest.py +++ b/tools/cellxgene_census_builder/tests/conftest.py @@ -116,7 +116,7 @@ def get_anndata( uns["batch_condition"] = np.array(["a", "b"], dtype="object") # Need to carefully set the corpora schema versions in order for tests to pass. - uns["schema_version"] = "5.0.0" # type: ignore + uns["schema_version"] = "5.1.0" # type: ignore return anndata.AnnData(X=X, obs=obs, var=var, obsm=obsm, uns=uns) diff --git a/tools/cellxgene_census_builder/tests/test_manifest.py b/tools/cellxgene_census_builder/tests/test_manifest.py index ab7a4384a..fb9098cea 100644 --- a/tools/cellxgene_census_builder/tests/test_manifest.py +++ b/tools/cellxgene_census_builder/tests/test_manifest.py @@ -65,7 +65,7 @@ def test_load_manifest_from_cxg(empty_blocklist: str) -> None: "collection_doi_label": "Publication 1", "citation": "citation", "title": "dataset #1", - "schema_version": "5.0.0", + "schema_version": "5.1.0", "assets": [ { "filesize": 123, @@ -90,7 +90,7 @@ def test_load_manifest_from_cxg(empty_blocklist: str) -> None: "collection_doi_label": "Publication 2", "citation": "citation", "title": "dataset #2", - "schema_version": "5.0.0", + "schema_version": "5.1.0", "assets": [{"filesize": 456, "filetype": "H5AD", "url": "https://fake.url/dataset_id_2.h5ad"}], "dataset_version_id": "dataset_id_2", "cell_count": 11, @@ -122,7 +122,7 @@ def test_load_manifest_from_cxg_errors_on_datasets_with_old_schema( "collection_doi_label": "Publication 1", "citation": "citation", "title": "dataset #1", - "schema_version": "5.0.0", + "schema_version": "5.1.0", "assets": [{"filesize": 123, "filetype": "H5AD", "url": "https://fake.url/dataset_id_1.h5ad"}], "dataset_version_id": "dataset_id_1", "cell_count": 10, @@ -166,7 +166,7 @@ def test_load_manifest_from_cxg_excludes_datasets_with_no_assets( "collection_doi": None, "citation": "citation", "title": "dataset #1", - "schema_version": "5.0.0", + "schema_version": "5.1.0", "assets": [{"filesize": 123, "filetype": "H5AD", "url": "https://fake.url/dataset_id_1.h5ad"}], "dataset_version_id": "dataset_id_1", "cell_count": 10, @@ -179,7 +179,7 @@ def test_load_manifest_from_cxg_excludes_datasets_with_no_assets( "collection_doi": None, "citation": "citation", "title": "dataset #2", - "schema_version": "5.0.0", + "schema_version": "5.1.0", "assets": [], "dataset_version_id": "dataset_id_2", "cell_count": 10, From 443845dcca4d9115284756af41f914b839cb64f7 Mon Sep 17 00:00:00 2001 From: Emanuele Bezzi Date: Fri, 28 Jun 2024 09:06:23 -0700 Subject: [PATCH 02/16] [docs] Update schema doc for version 2.1.0 (#1211) --- docs/cellxgene_census_schema.md | 45 ++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/docs/cellxgene_census_schema.md b/docs/cellxgene_census_schema.md index c5c7b098d..511899ec0 100644 --- a/docs/cellxgene_census_schema.md +++ b/docs/cellxgene_census_schema.md @@ -1,8 +1,8 @@ # CZ CELLxGENE Discover Census Schema -**Version**: 2.0.1 +**Version**: 2.1.0 -**Last edited**: March, 2024. +**Last edited**: June, 2024. The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED" "MAY", and "OPTIONAL" in this document are to be interpreted as described in [BCP 14](https://tools.ietf.org/html/bcp14), [RFC2119](https://www.rfc-editor.org/rfc/rfc2119.txt), and [RFC8174](https://www.rfc-editor.org/rfc/rfc8174.txt) when, and only when, they appear in all capitals, as shown here. @@ -10,14 +10,14 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S The CZ CELLxGENE Discover Census, hereafter referred as Census, is a versioned data object and API for most of the single-cell data hosted at [CZ CELLxGENE Discover](https://cellxgene.cziscience.com/). To learn more about the Census visit the `chanzuckerberg/cellxgene-census` [github repository](https://github.com/chanzuckerberg/cellxgene-census) -To better understand this document the reader should be familiar with the [CELLxGENE dataset schema](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md) and [SOMA](https://github.com/single-cell-data/SOMA/blob/main/abstract_specification.md). +To better understand this document the reader should be familiar with the [CELLxGENE dataset schema](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md) and [SOMA](https://github.com/single-cell-data/SOMA/blob/main/abstract_specification.md). ## Definitions The following terms are used throughout this document: * adata – generic variable name that refers to an [`AnnData`](https://anndata.readthedocs.io/) object. -* CELLxGENE dataset schema – the data schema for h5ad files served by CELLxGENE Discover, for this Census schema: [CELLxGENE dataset schema version is 5.0.0](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md) +* CELLxGENE dataset schema – the data schema for h5ad files served by CELLxGENE Discover, for this Census schema: [CELLxGENE dataset schema version is 5.1.0](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md) * census\_obj – the Census root object, a SOMACollection. * Census data release – a versioned Census object deposited in a public bucket and accessible by APIs. * tissue – original tissue annotation. @@ -44,23 +44,23 @@ Census data releases are versioned separately from the schema. ### Data included -All datasets included in the Census MUST be of [CELLxGENE dataset schema version 5.0.0](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md). The following data constraints are imposed on top of the CELLxGENE dataset schema. +All datasets included in the Census MUST be of [CELLxGENE dataset schema version 5.1.0](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md). The following data constraints are imposed on top of the CELLxGENE dataset schema. #### Species -The Census MUST only contain observations (cells) with an [`organism_ontology_term_id`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#organism_ontology_term_id) value of either "NCBITaxon:10090" for *Mus musculus* or "NCBITaxon:9606" for *Homo sapiens* MUST be included. +The Census MUST only contain observations (cells) with an [`organism_ontology_term_id`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#organism_ontology_term_id) value of either "NCBITaxon:10090" for *Mus musculus* or "NCBITaxon:9606" for *Homo sapiens* MUST be included. -The Census MUST only contain features (genes) with a [`feature_reference`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#feature_reference) value of either "NCBITaxon:10090" for *Mus musculus* or "NCBITaxon:9606" for *Homo sapiens* MUST be included +The Census MUST only contain features (genes) with a [`feature_reference`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#feature_reference) value of either "NCBITaxon:10090" for *Mus musculus* or "NCBITaxon:9606" for *Homo sapiens* MUST be included #### Multi-species data constraints -Per the CELLxGENE dataset schema, [multi-species datasets MAY contain observations (cells) of a given organism and features (genes) of a different one](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#general-requirements), as defined in [`organism_ontology_term_id`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#organism_ontology_term_id) and [`feature_reference`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#feature_reference) respectively. +Per the CELLxGENE dataset schema, [multi-species datasets MAY contain observations (cells) of a given organism and features (genes) of a different one](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#general-requirements), as defined in [`organism_ontology_term_id`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#organism_ontology_term_id) and [`feature_reference`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#feature_reference) respectively. For any given multi-species dataset, observation and features from the dataset are included in the Census as defined by the following: * Where a dataset includes observations and features from a single species, all observations and features from the dataset are included in the Census. * Where a dataset includes observations from a single species `S`, and includes features from multiple species *including* the species `S`, all dataset observations and all features from `S` will be included in the Census. -* Where a dataset includes features from a single species `S`, and observations from multiple species *including* the species `S`, all dataset features and all observations from speices `S` are included in the Census. +* Where a dataset includes features from a single species `S`, and observations from multiple species *including* the species `S`, all dataset features and all observations from species `S` are included in the Census. * Where a species has observations *AND* features from multiple species, the dataset will be excluded from the Census. The table below shows all possible combinations of organisms for both observations and features, assuming a Census comprised of Homo sapiens and Mus musculus. For each combination, inclusion criteria for the Census is provided. @@ -114,7 +114,7 @@ The table below shows all possible combinations of organisms for both observatio #### Assays -Assays are defined in the CELLxGENE dataset schema in [`assay_ontology_term_id`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#assay_ontology_term_id). +Assays are defined in the CELLxGENE dataset schema in [`assay_ontology_term_id`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#assay_ontology_term_id). The Census MUST include all cells from the list of [accepted assays](./census_accepted_assays.csv). @@ -143,15 +143,15 @@ These data need to be normalized by gene length for downstream analysis. #### Data matrix types -Per the CELLxGENE dataset schema, [all RNA assays MUST include UMI or read counts](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#x-matrix-layers). Author-normalized data layers [as defined in the CELLxGENE dataset schema](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#x-matrix-layers) MUST NOT be included in the Census. +Per the CELLxGENE dataset schema, [all RNA assays MUST include UMI or read counts](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#x-matrix-layers). Author-normalized data layers [as defined in the CELLxGENE dataset schema](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#x-matrix-layers) MUST NOT be included in the Census. #### Sample types -Only observations (cells) from primary tissue MUST be included in the Census. Thus, ONLY those observations with a [`tissue_type`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#tissue_type) value equal to "tissue" MUST be included; other values of `tissue_type` MUST NOT be included. +Only observations (cells) from primary tissue MUST be included in the Census. Thus, ONLY those observations with a [`tissue_type`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#tissue_type) value equal to "tissue" MUST be included; other values of `tissue_type` MUST NOT be included. #### Repeated data -When a cell is represented multiple times in CELLxGENE Discover, only one is marked as the primary cell. This is defined in the CELLxGENE dataset schema under [`is_primary_data`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#is_primary_data). This information MUST be included in the Census cell metadata to enable queries that retrieve datasets (see cell metadata below), and all cells MUST be included in the Census. +When a cell is represented multiple times in CELLxGENE Discover, only one is marked as the primary cell. This is defined in the CELLxGENE dataset schema under [`is_primary_data`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#is_primary_data). This information MUST be included in the Census cell metadata to enable queries that retrieve datasets (see cell metadata below), and all cells MUST be included in the Census. ### Data encoding and organization @@ -231,7 +231,7 @@ An example of this `SOMADataFrame` is shown below: dataset_schema_version - 5.0.0 + 5.1.0 total_cell_count @@ -283,6 +283,10 @@ All datasets used to build the Census MUST be included in a table modeled as a ` collection_doi string + + collection_doi_label + string + dataset_id string @@ -361,7 +365,7 @@ Summary cell counts grouped by organism and relevant cell metadata MUST be model unique_cell_count int - Unique number of cells for the combination of values of all other fields above. Unique number of cells refers to the cell count, for this group, when is_primary_data == True + Unique number of cells for the combination of values of all other fields above. Unique number of cells refers to the cell count, for this group, when is_primary_data == True @@ -656,7 +660,7 @@ For each organism the `SOMAExperiment` MUST contain the following: #### Matrix Data, count (raw) matrix – `census_obj["census_data"][organism].ms["RNA"].X["raw"]` – `SOMASparseNDArray` -Per the CELLxGENE dataset schema, [all RNA assays MUST include UMI or read counts](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#x-matrix-layers). These counts MUST be encoded as `float32` in this `SOMASparseNDArray` with a fill value of zero (0), and no explicitly stored zero values. +Per the CELLxGENE dataset schema, [all RNA assays MUST include UMI or read counts](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#x-matrix-layers). These counts MUST be encoded as `float32` in this `SOMASparseNDArray` with a fill value of zero (0), and no explicitly stored zero values. #### Matrix Data, normalized count matrix – `census_obj["census_data"][organism].ms["RNA"].X["normalized"]` – `SOMASparseNDArray` @@ -670,9 +674,9 @@ as `normalized[i,j] = X[i,j] / sum(X[i, ])`. #### Feature metadata – `census_obj["census_data"][organism].ms["RNA"].var` – `SOMADataFrame` -The Census MUST only contain features with a [`feature_biotype`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#feature_biotype) value of "gene". +The Census MUST only contain features with a [`feature_biotype`](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#feature_biotype) value of "gene". -The [gene references are pinned](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.0.0/schema.md#required-gene-annotations) as defined in the CELLxGENE dataset schema. +The [gene references are pinned](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md#required-gene-annotations) as defined in the CELLxGENE dataset schema. The following columns MUST be included: @@ -870,6 +874,11 @@ Cell metadata MUST be encoded as a `SOMADataFrame` with the following columns: ## Changelog +### Version 2.1.0 + +* Update to require [CELLxGENE schema version 5.1.0](https://github.com/chanzuckerberg/single-cell-curation/blob/main/schema/5.1.0/schema.md) +* Adds `collection_doi_label` to "Census table of CELLxGENE Discover datasets – `census_obj["census_info"]["datasets"]`" + ### Version 2.0.1 * Update accepted assays for Census based on guidance from curators. From 8d1e1033d3cfe154d0608cf6deb71e9a23a66035 Mon Sep 17 00:00:00 2001 From: Emanuele Bezzi Date: Fri, 28 Jun 2024 11:07:32 -0700 Subject: [PATCH 03/16] [python] Update scvi pipeline for the June LTS training (#1173) * Update scvi pipeline for the June LTS training * parametrize census config --- tools/models/scvi/scvi-config.yaml | 4 +++- tools/models/scvi/scvi-create-latent-update.py | 7 ++++--- tools/models/scvi/scvi-prepare.py | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/models/scvi/scvi-config.yaml b/tools/models/scvi/scvi-config.yaml index 656d45cd1..b8f017fc3 100644 --- a/tools/models/scvi/scvi-config.yaml +++ b/tools/models/scvi/scvi-config.yaml @@ -5,6 +5,8 @@ census: null obs_query_model: # Required when loading data for model training. Do not change. 'is_primary_data == True and nnz >= 300' + version: + "2024-05-20" hvg: top_n_hvg: 8000 @@ -19,7 +21,7 @@ model: filename: "scvi.model" n_hidden: 512 n_latent: 50 - n_layers: 1 + n_layers: 2 train: max_epochs: 100 batch_size: 1024 diff --git a/tools/models/scvi/scvi-create-latent-update.py b/tools/models/scvi/scvi-create-latent-update.py index 5aeaa34b6..0a70e2596 100644 --- a/tools/models/scvi/scvi-create-latent-update.py +++ b/tools/models/scvi/scvi-create-latent-update.py @@ -14,12 +14,13 @@ with open(file) as f: config = yaml.safe_load(f) - census = cellxgene_census.open_soma(census_version="2023-12-15") - census_config = config.get("census") experiment_name = census_config.get("organism") obs_value_filter = census_config.get("obs_query") + version = census_config.get("version") + census = cellxgene_census.open_soma(census_version=version) + hv = pd.read_pickle("hv_genes.pkl") hv_idx = hv[hv].index @@ -44,7 +45,7 @@ adata.var.set_index("feature_id", inplace=True) - idx = query.obs(column_names=["soma_joinid"]).concat().to_pandas().index.to_numpy() + idx = query.obs(column_names=["soma_joinid"]).concat().to_pandas().to_numpy() del census, query, hv, hv_idx gc.collect() diff --git a/tools/models/scvi/scvi-prepare.py b/tools/models/scvi/scvi-prepare.py index 4c2214629..b66c05833 100644 --- a/tools/models/scvi/scvi-prepare.py +++ b/tools/models/scvi/scvi-prepare.py @@ -11,13 +11,14 @@ with open(file) as f: config = yaml.safe_load(f) - census = cellxgene_census.open_soma(census_version="2023-12-15") - census_config = config.get("census") experiment_name = census_config.get("organism") obs_query = census_config.get("obs_query") obs_query_model = census_config.get("obs_query_model") + version = census_config.get("version") + census = cellxgene_census.open_soma(census_version=version) + if obs_query is None: obs_value_filter = obs_query_model else: From 8385dda24aa8372f105997cb04cf559ce8c287fd Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Fri, 28 Jun 2024 14:11:38 -0700 Subject: [PATCH 04/16] [python] Check census version for get_all_available_embeddings (#1207) * Check census version for get_all_available_embeddings * Add support for version aliases to get_embedding_metadata_by_name --- .../experimental/_embedding.py | 21 +++++++-- .../tests/experimental/test_embeddings.py | 44 ++++++++++++++++++- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/api/python/cellxgene_census/src/cellxgene_census/experimental/_embedding.py b/api/python/cellxgene_census/src/cellxgene_census/experimental/_embedding.py index 767b74e0b..da40da331 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/experimental/_embedding.py +++ b/api/python/cellxgene_census/src/cellxgene_census/experimental/_embedding.py @@ -18,7 +18,12 @@ import tiledbsoma as soma from .._open import get_default_soma_context, open_soma -from .._release_directory import CensusVersionDescription, CensusVersionName, get_census_version_directory +from .._release_directory import ( + CensusVersionDescription, + CensusVersionName, + get_census_version_description, + get_census_version_directory, +) CELL_CENSUS_EMBEDDINGS_MANIFEST_URL = "https://contrib.cellxgene.cziscience.com/contrib/cell-census/contributions.json" @@ -181,6 +186,9 @@ def get_embedding_metadata_by_name( ValueError: if no embeddings are found for the specified query parameters. """ + census_version_description = get_census_version_description(census_version) + resolved_census_version = census_version_description["release_build"] + response = requests.get(CELL_CENSUS_EMBEDDINGS_MANIFEST_URL) response.raise_for_status() @@ -191,12 +199,14 @@ def get_embedding_metadata_by_name( obj["embedding_name"] == embedding_name and obj["experiment_name"] == organism and obj["data_type"] == embedding_type - and obj["census_version"] == census_version + and obj["census_version"] == resolved_census_version ): embeddings.append(obj) if len(embeddings) == 0: - raise ValueError(f"No embeddings found for {embedding_name}, {organism}, {census_version}, {embedding_type}") + raise ValueError( + f"No embeddings found for {embedding_name}, {organism}, {resolved_census_version}, {embedding_type}" + ) return sorted(embeddings, key=lambda x: x["submission_date"])[-1] @@ -224,13 +234,16 @@ def get_all_available_embeddings(census_version: str) -> list[dict[str, Any]]: }] """ + # Validate census_version + census_version_description = get_census_version_description(census_version) + response = requests.get(CELL_CENSUS_EMBEDDINGS_MANIFEST_URL) response.raise_for_status() embeddings = [] manifest = response.json() for _, obj in manifest.items(): - if obj["census_version"] == census_version: + if obj["census_version"] == census_version_description["release_build"]: embeddings.append(obj) return embeddings diff --git a/api/python/cellxgene_census/tests/experimental/test_embeddings.py b/api/python/cellxgene_census/tests/experimental/test_embeddings.py index aeb0ff661..ac5bf3872 100644 --- a/api/python/cellxgene_census/tests/experimental/test_embeddings.py +++ b/api/python/cellxgene_census/tests/experimental/test_embeddings.py @@ -1,6 +1,9 @@ +from functools import partial + import pytest import requests_mock as rm +import cellxgene_census from cellxgene_census.experimental import ( get_all_available_embeddings, get_all_census_versions_with_embedding, @@ -74,6 +77,28 @@ def test_get_embedding_metadata_by_name(requests_mock: rm.Mocker) -> None: ) +def test_get_embedding_by_name_w_version_aliases() -> None: + """https://github.com/chanzuckerberg/cellxgene-census/issues/1202""" + # Only testing "stable" as "latest" doesn't have embeddings + version = "stable" + resolved_version = cellxgene_census.get_census_version_description(version)["release_build"] + + metadata = get_all_available_embeddings(version)[0] + + _get_metadata = partial( + get_embedding_metadata_by_name, + embedding_name=metadata["embedding_name"], + organism=metadata["experiment_name"], + embedding_type=metadata["data_type"], + ) + + w_alias = _get_metadata(census_version=version) + w_resolved = _get_metadata(census_version=resolved_version) + + assert w_resolved == w_alias + assert metadata == w_alias + + def test_get_all_available_embeddings(requests_mock: rm.Mocker) -> None: mock_embeddings = { "embedding-id-1": { @@ -108,8 +133,8 @@ def test_get_all_available_embeddings(requests_mock: rm.Mocker) -> None: assert embeddings is not None assert len(embeddings) == 2 - # Query for a non existing version of the Census - embeddings = get_all_available_embeddings("2024-12-15") + # Query for a version of the census that doesn't have embeddings + embeddings = get_all_available_embeddings("2023-05-15") assert len(embeddings) == 0 @@ -175,3 +200,18 @@ def test_get_all_census_versions_with_embedding(requests_mock: rm.Mocker) -> Non versions = get_all_census_versions_with_embedding("emb_2", organism="mus_musculus", embedding_type="var_embedding") assert versions == ["2023-12-15"] + + +@pytest.mark.parametrize("version", ["stable", "latest"]) +def test_get_all_available_embeddings_w_version_aliases(version: str) -> None: + """https://github.com/chanzuckerberg/cellxgene-census/issues/1202""" + resolved_version = cellxgene_census.get_census_version_description(version)["release_build"] + + assert get_all_available_embeddings(version) == get_all_available_embeddings(resolved_version) + + +def test_get_all_available_embeddings_non_existing_version() -> None: + false_version = "not a real version" + + with pytest.raises(ValueError, match=f"Unable to locate Census version: {false_version}"): + get_all_available_embeddings(false_version) From 14e440441a2590d2d22dd96e1e6c00cb031b00fe Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Fri, 28 Jun 2024 14:22:28 -0700 Subject: [PATCH 05/16] [python] Start move to {obs/var} specific arguments for get_anndata (#1149) * Start move to {obs/var} specific arguments for get_anndata * turn on pre-commit * Don't | types * Actually throw deprecation warning * With a comment * Fix warning type * update docs * its not it's Co-authored-by: Emanuele Bezzi --------- Co-authored-by: Isaac Virshup Co-authored-by: Emanuele Bezzi --- .../src/cellxgene_census/_get_anndata.py | 30 ++++++- .../tests/test_get_anndata.py | 78 ++++++++++++++++--- .../census_access_maintained_embeddings.ipynb | 2 +- .../api_demo/census_citation_generation.ipynb | 2 +- .../notebooks/api_demo/census_embedding.ipynb | 2 +- .../api_demo/census_query_extract.ipynb | 4 +- .../embeddings_qc_2023-12-15.ipynb | 8 +- 7 files changed, 103 insertions(+), 23 deletions(-) diff --git a/api/python/cellxgene_census/src/cellxgene_census/_get_anndata.py b/api/python/cellxgene_census/src/cellxgene_census/_get_anndata.py index 69ca0da6b..e37337184 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/_get_anndata.py +++ b/api/python/cellxgene_census/src/cellxgene_census/_get_anndata.py @@ -8,6 +8,7 @@ """ from typing import Literal, Optional, Sequence +from warnings import warn import anndata import pandas as pd @@ -38,6 +39,8 @@ def get_anndata( column_names: Optional[soma.AxisColumnNames] = None, obs_embeddings: Optional[Sequence[str]] = (), var_embeddings: Optional[Sequence[str]] = (), + obs_column_names: Optional[Sequence[str]] = None, + var_column_names: Optional[Sequence[str]] = None, ) -> anndata.AnnData: """Convenience wrapper around :class:`tiledbsoma.Experiment` query, to build and execute a query, and return it as an :class:`anndata.AnnData` object. @@ -65,8 +68,6 @@ def get_anndata( var_coords: Coordinates for the ``var`` axis, which is indexed by the ``soma_joinid`` value. May be an ``int``, a list of ``int``, or a slice. The default, ``None``, selects all. - column_names: - Columns to fetch for ``obs`` and ``var`` dataframes. obsm_layers: Additional obsm layers to read and return in the ``obsm`` slot. obsp_layers: @@ -83,6 +84,10 @@ def get_anndata( Additional embeddings to be returned as part of the ``varm`` slot. Use :func:`get_all_available_embeddings` to retrieve available embeddings for this Census version and organism. + obs_column_names: + Columns to fetch for ``obs`` dataframe. + var_column_names: + Columns to fetch for ``var`` dataframe. Returns: An :class:`anndata.AnnData` object containing the census slice. @@ -93,7 +98,7 @@ def get_anndata( Examples: >>> get_anndata(census, "Mus musculus", obs_value_filter="tissue_general in ['brain', 'lung']") - >>> get_anndata(census, "Homo sapiens", column_names={"obs": ["tissue"]}) + >>> get_anndata(census, "Homo sapiens", obs_column_names=["tissue"]) >>> get_anndata(census, "Homo sapiens", obs_coords=slice(0, 1000)) """ @@ -107,6 +112,23 @@ def get_anndata( if varm_layers and var_embeddings and set(varm_layers) & set(var_embeddings): raise ValueError("Cannot request both `varm_layers` and `var_embeddings` for the same embedding name") + # Backwards compat for old column_names argument + if column_names is not None: + if obs_column_names is not None or var_column_names is not None: + raise ValueError( + "Both the deprecated 'column_names' argument and its replacements were used. Please use 'obs_column_names' and 'var_column_names' only." + ) + else: + warn( + "The argument `column_names` is deprecated and will be removed in a future release. Please use `obs_column_names` and `var_column_names` instead.", + FutureWarning, + stacklevel=2, + ) + if "obs" in column_names: + obs_column_names = column_names["obs"] + if "var" in column_names: + var_column_names = column_names["var"] + with exp.axis_query( measurement_name, obs_query=soma.AxisQuery(value_filter=obs_value_filter, coords=obs_coords), @@ -114,7 +136,7 @@ def get_anndata( ) as query: adata = query.to_anndata( X_name=X_name, - column_names=column_names, + column_names={"obs": obs_column_names, "var": var_column_names}, X_layers=X_layers, obsm_layers=obsm_layers, varm_layers=varm_layers, diff --git a/api/python/cellxgene_census/tests/test_get_anndata.py b/api/python/cellxgene_census/tests/test_get_anndata.py index a2f0be1ce..71ef42eb7 100644 --- a/api/python/cellxgene_census/tests/test_get_anndata.py +++ b/api/python/cellxgene_census/tests/test_get_anndata.py @@ -25,16 +25,14 @@ def test_get_anndata_value_filter(census: soma.Collection) -> None: organism="Mus musculus", obs_value_filter="tissue_general == 'vasculature'", var_value_filter="feature_name in ['Gm53058', '0610010K14Rik']", - column_names={ - "obs": [ - "soma_joinid", - "cell_type", - "tissue", - "tissue_general", - "assay", - ], - "var": ["soma_joinid", "feature_id", "feature_name", "feature_length"], - }, + obs_column_names=[ + "soma_joinid", + "cell_type", + "tissue", + "tissue_general", + "assay", + ], + var_column_names=["soma_joinid", "feature_id", "feature_name", "feature_length"], ) assert ad is not None @@ -253,6 +251,66 @@ def test_get_anndata_obsm_layers_and_add_obs_embedding_fails(lts_census: soma.Co ) +@pytest.mark.live_corpus +def test_deprecated_column_api(census: soma.Collection) -> None: + """Testing for previous `column_names` argument. + + See: https://github.com/chanzuckerberg/cellxgene-census/issues/1035 + """ + ad_curr = cellxgene_census.get_anndata( + census, + organism="Mus musculus", + obs_value_filter="tissue_general == 'vasculature'", + var_value_filter="feature_name in ['Gm53058', '0610010K14Rik']", + obs_column_names=[ + "soma_joinid", + "cell_type", + "tissue", + "tissue_general", + "assay", + ], + var_column_names=["soma_joinid", "feature_id", "feature_name", "feature_length"], + ) + with pytest.warns(FutureWarning): + ad_prev = cellxgene_census.get_anndata( + census, + organism="Mus musculus", + obs_value_filter="tissue_general == 'vasculature'", + var_value_filter="feature_name in ['Gm53058', '0610010K14Rik']", + column_names={ + "obs": [ + "soma_joinid", + "cell_type", + "tissue", + "tissue_general", + "assay", + ], + "var": ["soma_joinid", "feature_id", "feature_name", "feature_length"], + }, + ) + with pytest.raises( + ValueError, match=r"Both the deprecated 'column_names' argument and its replacements were used." + ): + cellxgene_census.get_anndata( + census, + organism="Mus musculus", + obs_value_filter="tissue_general == 'vasculature'", + var_value_filter="feature_name in ['Gm53058', '0610010K14Rik']", + obs_column_names=[ + "soma_joinid", + "cell_type", + ], + column_names={ + "obs": [ + "soma_joinid", + "cell_type", + ], + }, + ) + pd.testing.assert_frame_equal(ad_curr.obs, ad_prev.obs) + pd.testing.assert_frame_equal(ad_curr.var, ad_prev.var) + + def _map_to_get_anndata_args(query: Dict[str, Any], axis: Literal["obs", "var"]) -> Dict[str, Any]: """Helper to map arguments of get_obs/ get_var to get_anndata.""" result = {} diff --git a/api/python/notebooks/api_demo/census_access_maintained_embeddings.ipynb b/api/python/notebooks/api_demo/census_access_maintained_embeddings.ipynb index 0d4382b3b..c2f200e96 100644 --- a/api/python/notebooks/api_demo/census_access_maintained_embeddings.ipynb +++ b/api/python/notebooks/api_demo/census_access_maintained_embeddings.ipynb @@ -181,7 +181,7 @@ " organism=\"homo_sapiens\",\n", " measurement_name=\"RNA\",\n", " obs_value_filter=\"tissue_general == 'central nervous system'\",\n", - " column_names={\"obs\": [\"cell_type\"]},\n", + " obs_column_names=[\"cell_type\"],\n", " obs_embeddings=emb_names,\n", ")\n", "\n", diff --git a/api/python/notebooks/api_demo/census_citation_generation.ipynb b/api/python/notebooks/api_demo/census_citation_generation.ipynb index eb849e20c..9c50e3728 100644 --- a/api/python/notebooks/api_demo/census_citation_generation.ipynb +++ b/api/python/notebooks/api_demo/census_citation_generation.ipynb @@ -325,7 +325,7 @@ " measurement_name=\"RNA\",\n", " obs_value_filter=\"tissue == 'cardiac atrium'\",\n", " var_value_filter=\"feature_name == 'MYBPC3'\",\n", - " column_names={\"obs\": [\"dataset_id\", \"cell_type\"]},\n", + " obs_column_names=[\"dataset_id\", \"cell_type\"],\n", ")\n", "\n", "# Get a citation string for the slice\n", diff --git a/api/python/notebooks/api_demo/census_embedding.ipynb b/api/python/notebooks/api_demo/census_embedding.ipynb index e8a1df604..0d714380e 100644 --- a/api/python/notebooks/api_demo/census_embedding.ipynb +++ b/api/python/notebooks/api_demo/census_embedding.ipynb @@ -243,7 +243,7 @@ " organism=EXPERIMENT_NAME,\n", " measurement_name=MEASUREMENT_NAME,\n", " obs_value_filter=\"tissue_general == 'central nervous system'\",\n", - " column_names={\"obs\": [\"cell_type\", \"soma_joinid\"]},\n", + " obs_column_names=[\"cell_type\", \"soma_joinid\"],\n", " obs_embeddings=[\"scgpt\"],\n", " )" ] diff --git a/api/python/notebooks/api_demo/census_query_extract.ipynb b/api/python/notebooks/api_demo/census_query_extract.ipynb index 828dd687d..3cdaf5b14 100644 --- a/api/python/notebooks/api_demo/census_query_extract.ipynb +++ b/api/python/notebooks/api_demo/census_query_extract.ipynb @@ -65,7 +65,7 @@ "\n", "The method will return an `anndata.AnnData` object, it takes as an input a census object, the string for an organism, and for both cell and gene metadata we can specify filters and column selection as described above but with the following arguments:\n", "\n", - "- `column_names` — a dictionary with two keys `obs` and `var` whose values are lists of strings indicating the columns to select for cell and gene metadata respectively.\n", + "- `obs_column_names` and `var_column_names` — a pair of arguments whose values are lists of strings indicating the columns to select for cell (`obs`) and gene (`var`) metadata respectively.\n", "- `obs_value_filter` — python expression with selection conditions to fetch **cells** meeting a criteria. For full details see [tiledb.QueryCondition](https://tiledb-inc-tiledb.readthedocs-hosted.com/projects/tiledb-py/en/stable/python-api.html#query-condition).\n", "- `var_value_filter` — python expression with selection conditions to fetch **genes** meeting a criteria. Details as above. For full details see [tiledb.QueryCondition](https://tiledb-inc-tiledb.readthedocs-hosted.com/projects/tiledb-py/en/stable/python-api.html#query-condition).\n", "\n", @@ -95,7 +95,7 @@ " organism=\"Homo sapiens\",\n", " var_value_filter=\"feature_id in ['ENSG00000161798', 'ENSG00000188229']\",\n", " obs_value_filter=\"cell_type == 'B cell' and tissue_general == 'lung' and disease == 'COVID-19' and is_primary_data == True\",\n", - " column_names={\"obs\": [\"sex\"]},\n", + " obs_column_names=[\"sex\"],\n", ")" ] }, diff --git a/tools/census_contrib_qc/embeddings_qc_2023-12-15.ipynb b/tools/census_contrib_qc/embeddings_qc_2023-12-15.ipynb index 7a32a46d5..0bc917ece 100644 --- a/tools/census_contrib_qc/embeddings_qc_2023-12-15.ipynb +++ b/tools/census_contrib_qc/embeddings_qc_2023-12-15.ipynb @@ -488,7 +488,7 @@ " organism=\"homo_sapiens\",\n", " measurement_name=\"RNA\",\n", " obs_value_filter=\"tissue_general == 'central nervous system' and is_primary_data == True\",\n", - " column_names={\"obs\": [\"cell_type\", \"assay\", \"soma_joinid\"]},\n", + " obs_column_names=[\"cell_type\", \"assay\", \"soma_joinid\"],\n", " obsm_layers=maintained_embs_human,\n", ")" ] @@ -663,7 +663,7 @@ " organism=\"homo_sapiens\",\n", " measurement_name=\"RNA\",\n", " obs_value_filter=\"tissue_general == 'pancreas' and is_primary_data == True\",\n", - " column_names={\"obs\": [\"cell_type\", \"assay\", \"soma_joinid\", \"dataset_id\", \"is_primary_data\"]},\n", + " obs_column_names=[\"cell_type\", \"assay\", \"soma_joinid\", \"dataset_id\", \"is_primary_data\"],\n", " obsm_layers=maintained_embs_human,\n", ")" ] @@ -862,7 +862,7 @@ " organism=\"mus_musculus\",\n", " measurement_name=\"RNA\",\n", " obs_value_filter=\"tissue_general == 'heart' and is_primary_data == True\",\n", - " column_names={\"obs\": [\"cell_type\", \"assay\", \"soma_joinid\"]},\n", + " obs_column_names=[\"cell_type\", \"assay\", \"soma_joinid\"],\n", " obsm_layers=maintained_embs_mouse,\n", ")" ] @@ -1014,7 +1014,7 @@ " organism=\"mus_musculus\",\n", " measurement_name=\"RNA\",\n", " obs_value_filter=\"tissue_general == 'pancreas'\",\n", - " column_names={\"obs\": [\"cell_type\", \"assay\", \"soma_joinid\"]},\n", + " obs_column_names=[\"cell_type\", \"assay\", \"soma_joinid\"],\n", " obsm_layers=maintained_embs_mouse,\n", ")" ] From f775282b284b170c29d466e44705b7e0b8394e90 Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Tue, 2 Jul 2024 12:57:48 -0700 Subject: [PATCH 06/16] [python] Stop including `"do_not_delete"` key in result of `get_census_version_directory` (#1215) * Fixed supposed CensusVersionDescription including a do_not_delete key * add a test --- .../src/cellxgene_census/_release_directory.py | 9 +++++++-- .../cellxgene_census/tests/test_directory.py | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/api/python/cellxgene_census/src/cellxgene_census/_release_directory.py b/api/python/cellxgene_census/src/cellxgene_census/_release_directory.py index e0839aab8..a5da8f08b 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/_release_directory.py +++ b/api/python/cellxgene_census/src/cellxgene_census/_release_directory.py @@ -9,7 +9,7 @@ import typing from collections import OrderedDict -from typing import Dict, Literal, Optional, Union, cast +from typing import Any, Dict, Literal, Optional, Union, cast import requests from typing_extensions import NotRequired, TypedDict @@ -353,7 +353,7 @@ def get_census_version_directory( response = requests.get(CELL_CENSUS_RELEASE_DIRECTORY_URL) response.raise_for_status() - directory: CensusDirectory = cast(CensusDirectory, response.json()) + directory: dict[str, str | dict[str, Any]] = response.json() directory_out: CensusDirectory = {} aliases: typing.Set[CensusVersionName] = set() @@ -379,6 +379,11 @@ def get_census_version_directory( if not isinstance(directory_value, dict): continue + # Filter fields + directory_value = { + k: directory_value[k] for k in CensusVersionDescription.__annotations__ if k in directory_value + } + # filter by release flags census_version_description = cast(CensusVersionDescription, directory_value) release_flags = cast(ReleaseFlags, {"lts": lts, "retracted": retracted}) diff --git a/api/python/cellxgene_census/tests/test_directory.py b/api/python/cellxgene_census/tests/test_directory.py index 9ac52f6ea..14a29bb9b 100644 --- a/api/python/cellxgene_census/tests/test_directory.py +++ b/api/python/cellxgene_census/tests/test_directory.py @@ -34,7 +34,6 @@ "release_date": "2022-09-30", "release_build": "2022-09-01", "flags": {"lts": True, "retracted": True}, - "do_not_delete": True, "retraction": { "date": "2022-11-15", "reason": "mistakes happen", @@ -53,7 +52,6 @@ "2022-11-01": { "release_date": "2022-11-30", "release_build": "2022-11-01", - "do_not_delete": True, "soma": { "uri": "s3://cellxgene-data-public/cell-census/2022-11-01/soma/", "s3_region": "us-west-2", @@ -188,3 +186,17 @@ def test_live_directory_contents() -> None: assert fs.exists(version_description["soma"]["uri"]) assert fs.exists(version_description["h5ads"]["uri"]) + + +def test_census_version_types() -> None: + """Do a little bit of runtime type checking on the results of census version functions. + + Part of solving: https://github.com/chanzuckerberg/cellxgene-census/issues/1204 + """ + from cellxgene_census._release_directory import CensusVersionDescription + + directory = cellxgene_census.get_census_version_directory() + for k, v in directory.items(): + assert set(v).issubset(CensusVersionDescription.__annotations__) + desc = cellxgene_census.get_census_version_description(k) + assert set(desc).issubset(CensusVersionDescription.__annotations__) From e35ed7e21a055d83d22dee4479737dd576361e32 Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Wed, 3 Jul 2024 10:08:43 -0700 Subject: [PATCH 07/16] [python] set user-agent (#1193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make source changes * Tests sorta working 🎉 * have most tests working i think * Try to generate certs in test setup * Simplify some test environment patching * Get tests working (except when the segfault) and clean up tiledb config management * Write my log to its own location * Remove some cruft from the proxy plugin * Fix segfaults? * Minor cleanup * Cleanup proxy setup * Nicer testing for user agent * Don't cache certificates * Clean up log checks, add test for embeddings * Some type stuff * Improve typing a bit * Make the proxy a bit more quiet * Update api/python/cellxgene_census/tests/test_user_agent.py Co-authored-by: Mike Lin * Update api/python/cellxgene_census/src/cellxgene_census/_testing/logger_proxy.py * Add user-agent test for pytorch loaders * Add environment variable to test jobs --------- Co-authored-by: Mike Lin --- .github/workflows/docsite-build-deploy.yml | 4 + .github/workflows/full-unittests.yml | 3 + .github/workflows/lts-compat-check.yml | 5 +- .github/workflows/profiler.yml | 3 + .github/workflows/py-dependency-check.yml | 3 + .github/workflows/py-formatting.yml | 3 + .github/workflows/py-unittests.yml | 3 + .github/workflows/r-check.yml | 3 + .github/workflows/r-dependency-check.yml | 3 + .../scripts/requirements-dev.txt | 1 + .../src/cellxgene_census/_open.py | 14 +- .../cellxgene_census/_release_directory.py | 6 +- .../src/cellxgene_census/_testing/__init__.py | 0 .../cellxgene_census/_testing/logger_proxy.py | 40 +++ .../src/cellxgene_census/_util.py | 14 + .../experimental/_embedding.py | 8 +- api/python/cellxgene_census/tests/conftest.py | 8 +- .../cellxgene_census/tests/test_user_agent.py | 284 ++++++++++++++++++ 18 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 api/python/cellxgene_census/src/cellxgene_census/_testing/__init__.py create mode 100644 api/python/cellxgene_census/src/cellxgene_census/_testing/logger_proxy.py create mode 100644 api/python/cellxgene_census/tests/test_user_agent.py diff --git a/.github/workflows/docsite-build-deploy.yml b/.github/workflows/docsite-build-deploy.yml index 7628e6760..f4a7e8166 100644 --- a/.github/workflows/docsite-build-deploy.yml +++ b/.github/workflows/docsite-build-deploy.yml @@ -6,6 +6,10 @@ on: workflow_dispatch: # Used to make post-release docfixes permissions: contents: write + +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + jobs: build-and-deploy: concurrency: ci-${{ github.ref }} diff --git a/.github/workflows/full-unittests.yml b/.github/workflows/full-unittests.yml index 87dc388a3..b41352d67 100644 --- a/.github/workflows/full-unittests.yml +++ b/.github/workflows/full-unittests.yml @@ -37,6 +37,9 @@ on: default: "" type: string +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + jobs: py_unit_tests: runs-on: single-cell-1tb-runner diff --git a/.github/workflows/lts-compat-check.yml b/.github/workflows/lts-compat-check.yml index 7e4a7b640..5fc787f0b 100644 --- a/.github/workflows/lts-compat-check.yml +++ b/.github/workflows/lts-compat-check.yml @@ -4,7 +4,10 @@ on: schedule: - cron: "30 1 * * *" workflow_dispatch: # used for debugging or manual validation - + +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + jobs: python-compat-check: name: Python LTS compatibility check diff --git a/.github/workflows/profiler.yml b/.github/workflows/profiler.yml index 2820d9bba..4a1c13364 100644 --- a/.github/workflows/profiler.yml +++ b/.github/workflows/profiler.yml @@ -1,5 +1,8 @@ name: Profiler +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + on: pull_request: paths: diff --git a/.github/workflows/py-dependency-check.yml b/.github/workflows/py-dependency-check.yml index daa85c09a..40aa4a16c 100644 --- a/.github/workflows/py-dependency-check.yml +++ b/.github/workflows/py-dependency-check.yml @@ -14,6 +14,9 @@ on: - cron: "30 1 * * *" workflow_dispatch: # used for debugging or manual validation +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + jobs: python-dependency-check: name: python-dependency-check diff --git a/.github/workflows/py-formatting.yml b/.github/workflows/py-formatting.yml index 5cf529795..25e877c26 100644 --- a/.github/workflows/py-formatting.yml +++ b/.github/workflows/py-formatting.yml @@ -7,6 +7,9 @@ on: push: branches: [main] +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + jobs: pre_commit_checks: name: pre-commit checks diff --git a/.github/workflows/py-unittests.yml b/.github/workflows/py-unittests.yml index 115083909..404902c38 100644 --- a/.github/workflows/py-unittests.yml +++ b/.github/workflows/py-unittests.yml @@ -12,6 +12,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + jobs: unit_tests_python_api: strategy: diff --git a/.github/workflows/r-check.yml b/.github/workflows/r-check.yml index 9a1397bfb..fad2ca8e5 100644 --- a/.github/workflows/r-check.yml +++ b/.github/workflows/r-check.yml @@ -8,6 +8,9 @@ on: push: branches: [main] +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + jobs: build: strategy: diff --git a/.github/workflows/r-dependency-check.yml b/.github/workflows/r-dependency-check.yml index 2017e4013..ccc5e2a4d 100644 --- a/.github/workflows/r-dependency-check.yml +++ b/.github/workflows/r-dependency-check.yml @@ -8,6 +8,9 @@ on: - cron: "30 1 * * *" workflow_dispatch: # used for debugging or manual validation +env: + CELLXGENE_CENSUS_USERAGENT: "CZI-test" + jobs: r-dependency-check: name: r-dependency-check diff --git a/api/python/cellxgene_census/scripts/requirements-dev.txt b/api/python/cellxgene_census/scripts/requirements-dev.txt index c04490c25..e80038f06 100644 --- a/api/python/cellxgene_census/scripts/requirements-dev.txt +++ b/api/python/cellxgene_census/scripts/requirements-dev.txt @@ -7,3 +7,4 @@ nbqa transformers[torch] git+https://huggingface.co/ctheodoris/Geneformer@8df5dc1 owlready2 +proxy.py diff --git a/api/python/cellxgene_census/src/cellxgene_census/_open.py b/api/python/cellxgene_census/src/cellxgene_census/_open.py index 640d2d9a6..642e6fbb6 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/_open.py +++ b/api/python/cellxgene_census/src/cellxgene_census/_open.py @@ -24,10 +24,14 @@ _get_census_mirrors, get_census_version_description, ) -from ._util import _uri_join +from ._util import _uri_join, _user_agent DEFAULT_CENSUS_VERSION = "stable" +DEFAULT_S3FS_KWARGS = { + "anon": True, + "cache_regions": True, +} DEFAULT_TILEDB_CONFIGURATION: Dict[str, Any] = { # https://docs.tiledb.com/main/how-to/configuration#configuration-parameters "py.init_buffer_bytes": 1 * 1024**3, @@ -120,7 +124,9 @@ def get_default_soma_context(tiledb_config: Optional[Dict[str, Any]] = None) -> Lifecycle: experimental """ - tiledb_config = dict(DEFAULT_TILEDB_CONFIGURATION, **(tiledb_config or {})) + tiledb_config = dict( + DEFAULT_TILEDB_CONFIGURATION, **{"vfs.s3.custom_headers.User-Agent": _user_agent()}, **(tiledb_config or {}) + ) return soma.options.SOMATileDBContext().replace(tiledb_config=tiledb_config) @@ -343,8 +349,8 @@ def download_source_h5ad( assert protocol == "s3" fs = s3fs.S3FileSystem( - anon=True, - cache_regions=True, + config_kwargs={"user_agent": _user_agent()}, + **DEFAULT_S3FS_KWARGS, ) fs.get_file( locator["uri"], diff --git a/api/python/cellxgene_census/src/cellxgene_census/_release_directory.py b/api/python/cellxgene_census/src/cellxgene_census/_release_directory.py index a5da8f08b..5ba8b77fb 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/_release_directory.py +++ b/api/python/cellxgene_census/src/cellxgene_census/_release_directory.py @@ -14,6 +14,8 @@ import requests from typing_extensions import NotRequired, TypedDict +from cellxgene_census._util import _user_agent + """ The following types describe the expected directory of Census builds, used to bootstrap all data location requests. @@ -350,7 +352,7 @@ def get_census_version_directory( } } """ - response = requests.get(CELL_CENSUS_RELEASE_DIRECTORY_URL) + response = requests.get(CELL_CENSUS_RELEASE_DIRECTORY_URL, headers={"User-Agent": _user_agent()}) response.raise_for_status() directory: dict[str, str | dict[str, Any]] = response.json() @@ -430,6 +432,6 @@ def get_census_mirror_directory() -> Dict[CensusMirrorName, CensusMirror]: def _get_census_mirrors() -> CensusMirrors: - response = requests.get(CELL_CENSUS_MIRRORS_DIRECTORY_URL) + response = requests.get(CELL_CENSUS_MIRRORS_DIRECTORY_URL, headers={"User-Agent": _user_agent()}) response.raise_for_status() return cast(CensusMirrors, response.json()) diff --git a/api/python/cellxgene_census/src/cellxgene_census/_testing/__init__.py b/api/python/cellxgene_census/src/cellxgene_census/_testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/python/cellxgene_census/src/cellxgene_census/_testing/logger_proxy.py b/api/python/cellxgene_census/src/cellxgene_census/_testing/logger_proxy.py new file mode 100644 index 000000000..c5e6ef54f --- /dev/null +++ b/api/python/cellxgene_census/src/cellxgene_census/_testing/logger_proxy.py @@ -0,0 +1,40 @@ +"""This module defines a plugin class that logs each request to a logfile. + +This class needs to be importable by the proxy server which runs in a separate process. +See the user agent tests for usage. +""" + +import json +import traceback +from pathlib import Path + +import proxy +from proxy.common.flag import flags + +flags.add_argument( + "--request-log-file", + type=str, + default="", + help="Where to log the requests to.", +) + + +class RequestLoggerPlugin(proxy.http.proxy.HttpProxyBasePlugin): # type: ignore + def handle_client_request(self, request: proxy.http.parser.HttpParser) -> proxy.http.parser.HttpParser: + # If anything fails in here, it just fails to respond + try: + with Path(self.flags.request_log_file).open("a") as f: + record = { + "method": request.method.decode(), + "url": str(request._url), + } + + if request.headers: + record["headers"] = {k2.decode().lower(): v.decode() for _, (k2, v) in request.headers.items()} + f.write(f"{json.dumps(record)}\n") + except Exception as e: + # Making sure there is some visible output + print(repr(e)) + traceback.print_exception(e) + raise e + return request diff --git a/api/python/cellxgene_census/src/cellxgene_census/_util.py b/api/python/cellxgene_census/src/cellxgene_census/_util.py index b7f70ee2c..70e979294 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/_util.py +++ b/api/python/cellxgene_census/src/cellxgene_census/_util.py @@ -2,6 +2,9 @@ import tiledbsoma as soma +USER_AGENT_ENVVAR = "CELLXGENE_CENSUS_USERAGENT" +"""Environment variable used to add more information into the user-agent.""" + def _uri_join(base: str, url: str) -> str: """Like urllib.parse.urljoin, but doesn't get confused by s3://.""" @@ -30,3 +33,14 @@ def _extract_census_version(census: soma.Collection) -> str: raise ValueError("Unable to extract Census version.") from None return version + + +def _user_agent() -> str: + import os + + import cellxgene_census + + if env_specifier := os.environ.get(USER_AGENT_ENVVAR, None): + return f"cellxgene-census-python/{cellxgene_census.__version__} {env_specifier}" + else: + return f"cellxgene-census-python/{cellxgene_census.__version__}" diff --git a/api/python/cellxgene_census/src/cellxgene_census/experimental/_embedding.py b/api/python/cellxgene_census/src/cellxgene_census/experimental/_embedding.py index da40da331..4baba8e06 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/experimental/_embedding.py +++ b/api/python/cellxgene_census/src/cellxgene_census/experimental/_embedding.py @@ -17,6 +17,8 @@ import requests import tiledbsoma as soma +from cellxgene_census._util import _user_agent + from .._open import get_default_soma_context, open_soma from .._release_directory import ( CensusVersionDescription, @@ -189,7 +191,7 @@ def get_embedding_metadata_by_name( census_version_description = get_census_version_description(census_version) resolved_census_version = census_version_description["release_build"] - response = requests.get(CELL_CENSUS_EMBEDDINGS_MANIFEST_URL) + response = requests.get(CELL_CENSUS_EMBEDDINGS_MANIFEST_URL, headers={"User-Agent": _user_agent()}) response.raise_for_status() manifest = cast(Dict[str, Dict[str, Any]], response.json()) @@ -237,7 +239,7 @@ def get_all_available_embeddings(census_version: str) -> list[dict[str, Any]]: # Validate census_version census_version_description = get_census_version_description(census_version) - response = requests.get(CELL_CENSUS_EMBEDDINGS_MANIFEST_URL) + response = requests.get(CELL_CENSUS_EMBEDDINGS_MANIFEST_URL, headers={"User-Agent": _user_agent()}) response.raise_for_status() embeddings = [] @@ -265,7 +267,7 @@ def get_all_census_versions_with_embedding( Returns: A list of census versions that contain the specified embedding. """ - response = requests.get(CELL_CENSUS_EMBEDDINGS_MANIFEST_URL) + response = requests.get(CELL_CENSUS_EMBEDDINGS_MANIFEST_URL, headers={"User-Agent": _user_agent()}) response.raise_for_status() manifest = response.json() diff --git a/api/python/cellxgene_census/tests/conftest.py b/api/python/cellxgene_census/tests/conftest.py index b6019d6d0..cbdfd969e 100644 --- a/api/python/cellxgene_census/tests/conftest.py +++ b/api/python/cellxgene_census/tests/conftest.py @@ -1,8 +1,13 @@ +import multiprocessing + import pytest import tiledbsoma as soma TEST_MARKERS_SKIPPED_BY_DEFAULT = ["expensive", "experimental"] +# tiledb will complain if this isn't set and a process is spawned. May cause segfaults on the proxy test if this isn't set. +multiprocessing.set_start_method("spawn", force=True) + def pytest_addoption(parser: pytest.Parser) -> None: for test_option in TEST_MARKERS_SKIPPED_BY_DEFAULT: @@ -51,9 +56,6 @@ def small_mem_context() -> soma.SOMATileDBContext: return get_default_soma_context(tiledb_config={"soma.init_buffer_bytes": 32 * 1024**2}) -# Fixtures for census objects - - @pytest.fixture(scope="session") def census() -> soma.Collection: import cellxgene_census diff --git a/api/python/cellxgene_census/tests/test_user_agent.py b/api/python/cellxgene_census/tests/test_user_agent.py new file mode 100644 index 000000000..dc410df9a --- /dev/null +++ b/api/python/cellxgene_census/tests/test_user_agent.py @@ -0,0 +1,284 @@ +# mypy: ignore-errors +from __future__ import annotations + +import json +import os +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING, Callable + +import numpy as np +import proxy +import pytest +import requests +from urllib3.exceptions import InsecureRequestWarning + +if TYPE_CHECKING: + from _pytest.tmpdir import TempPathFactory + +import cellxgene_census + +# We are forcing the requests to be insecure so we can intercept them. +pytestmark = pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning") + + +class ProxyInstance: + def __init__(self, proxy_obj: proxy.Proxy, logpth: Path): + self.proxy = proxy_obj + self.logpth = logpth + + @property + def port(self) -> int: + return self.proxy.flags.port + + +@pytest.fixture(scope="session") +def ca_certificates(tmp_path_factory: TempPathFactory) -> tuple[Path, Path, Path]: + # Adapted from https://github.com/abhinavsingh/proxy.py/blob/a7077cf8db3bb66a6667a9d968a401e8f805e092/Makefile#L68C1-L82C49 + # TODO: Figure out if we can remove this. Currently seems neccesary for intercepting tiledb s3 requests + cert_dir = tmp_path_factory.mktemp("ca-certificates") + KEY_FILE = cert_dir / "ca-key.pem" + CERT_FILE = cert_dir / "ca-cert.pem" + SIGNING_KEY_FILE = cert_dir / "ca-signing-key.pem" + assert proxy.common.pki.gen_private_key(key_path=KEY_FILE, password="proxy.py") + assert proxy.common.pki.remove_passphrase(key_in_path=KEY_FILE, password="proxy.py", key_out_path=KEY_FILE) + assert proxy.common.pki.gen_public_key( + public_key_path=CERT_FILE, private_key_path=KEY_FILE, private_key_password="proxy.py", subject="/CN=localhost" + ) + assert proxy.common.pki.gen_private_key(key_path=SIGNING_KEY_FILE, password="proxy.py") + assert proxy.common.pki.remove_passphrase( + key_in_path=SIGNING_KEY_FILE, password="proxy.py", key_out_path=SIGNING_KEY_FILE + ) + return (KEY_FILE, CERT_FILE, SIGNING_KEY_FILE) + + +@pytest.fixture(scope="session") +def proxy_server( + tmp_path_factory: TempPathFactory, + ca_certificates: tuple[Path, Path, Path], +): + import cellxgene_census + + tmp_path = tmp_path_factory.mktemp("proxy_logs") + # proxy.py can override passed ca-key-file and ca-cert-file with cached ones. So we create a fresh cache for each proxy server + cert_cache_dir = tmp_path_factory.mktemp("certificates_cache") + proxy_log_file = tmp_path / "proxy.log" + request_log_file = tmp_path / "proxy_requests.log" + key_file, cert_file, signing_keyfile = ca_certificates + assert all(p.is_file() for p in (key_file, cert_file, signing_keyfile)) + + # Adapted from TestCase setup from proxy.py: https://github.com/abhinavsingh/proxy.py/blob/develop/proxy/testing/test_case.py#L23 + PROXY_PY_STARTUP_FLAGS = [ + "--num-workers", + "1", + "--num-acceptors", + "1", + "--hostname", + "127.0.0.1", + "--port", + "0", + "--plugin", + "cellxgene_census._testing.logger_proxy.RequestLoggerPlugin", + "--ca-key-file", + str(key_file), + "--ca-cert-file", + str(cert_file), + "--ca-signing-key-file", + str(signing_keyfile), + "--ca-cert-dir", + str(cert_cache_dir), + "--log-file", + str(proxy_log_file), + "--request-log-file", + str(request_log_file), + ] + proxy_obj = proxy.Proxy(PROXY_PY_STARTUP_FLAGS) + with proxy_obj: + assert proxy_obj.acceptors + proxy.TestCase.wait_for_server(proxy_obj.flags.port) + proxy_instance = ProxyInstance(proxy_obj, request_log_file) + + # Now that proxy is set up, set relevant environment variables/ constants to make all request making libraries use proxy + with pytest.MonkeyPatch.context() as mp: + # Both requests and s3fs use these environment variables: + mp.setenv("HTTP_PROXY", f"http://localhost:{proxy_obj.flags.port}") + mp.setenv("HTTPS_PROXY", f"http://localhost:{proxy_obj.flags.port}") + + # s3fs + mp.setattr( + cellxgene_census._open, + "DEFAULT_S3FS_KWARGS", + { + "anon": True, + "cache_regions": True, + "use_ssl": False, # So we can inspect the requests on the proxy + }, + ) + + # requests + mp.setattr(requests, "get", partial(requests.request, "get", verify=False)) + + # tiledb + tiledb_config = cellxgene_census._open.DEFAULT_TILEDB_CONFIGURATION.copy() + tiledb_config["vfs.s3.proxy_host"] = "localhost" + tiledb_config["vfs.s3.proxy_port"] = str(proxy_instance.port) + tiledb_config["vfs.s3.verify_ssl"] = "false" + mp.setattr( + cellxgene_census._open, + "DEFAULT_TILEDB_CONFIGURATION", + tiledb_config, + ) + + yield proxy_instance + + +@pytest.fixture +def test_specific_useragent() -> str: + """Sets custom user agent addendum for every test so they can be uniqueley identified.""" + current_test_name = os.environ["PYTEST_CURRENT_TEST"] + with pytest.MonkeyPatch.context() as mp: + mp.setenv("CELLXGENE_CENSUS_USERAGENT", current_test_name) + yield current_test_name + + +@pytest.fixture() +def collect_proxy_requests(proxy_server: ProxyInstance): + """Test specific fixture exposing the proxy server. + + While a proxy server is started for every test session, this fixture + captures only the output that is written for a specific tests. These + logged requests can be checked to make sure have the correct headers. + """ + # If logs have already been written, count how many + if proxy_server.logpth.is_file(): + with proxy_server.logpth.open("r") as f: + prev_lines = len(f.readlines()) + else: + prev_lines = 0 + + def _proxy_requests(): + # For each new log written by the test, check that the correct headers were written + with proxy_server.logpth.open("r") as f: + records = [json.loads(line) for line in f.readlines()] + records = records[prev_lines:] + return records + + # Run test + yield _proxy_requests + + +@pytest.fixture(scope="session") +def small_dataset_id() -> str: + with cellxgene_census.open_soma(census_version="latest") as census: + census_datasets = census["census_info"]["datasets"].read().concat().to_pandas() + + small_dataset = census_datasets.nsmallest(1, "dataset_total_cell_count").iloc[0] + assert isinstance(small_dataset.dataset_id, str) + return small_dataset.dataset_id + + +def check_proxy_records(records: list[dict], *, custom_user_agent: None | str = None, min_records: int = 1) -> None: + # Check that there aren't two CONNECT requests in a row + prev_was_connect = False + for record in records: + was_connect = record["method"] == "CONNECT" + if prev_was_connect and was_connect: + raise AssertionError( + "Recieved multiple connect requests in a row. Some calls aren't being intercepted by the proxy." + ) + + # Check that headers were set correctly on intercepted requests + n_records = 0 + for record in records: + if record["method"] == "CONNECT": + continue + n_records += 1 + headers = record["headers"] + user_agent = headers["user-agent"] + assert "cellxgene-census-python" in user_agent + assert cellxgene_census.__version__ in user_agent + if custom_user_agent: + assert custom_user_agent in user_agent + assert n_records >= min_records, f"Fewer than min_records ({min_records}) were found." + + +def test_proxy_fixture(collect_proxy_requests: Callable[[], list[dict]]): + """Test that our proxy testing setup is working as expected.""" + # Should just be downloading a json + with pytest.warns(InsecureRequestWarning): + _ = cellxgene_census.get_census_version_directory() + + records = collect_proxy_requests() + + # Expecting a CONNECT request followed by a GET request + assert len(records) == 2 + assert records[0]["method"] == "CONNECT" + assert records[1]["method"] == "GET" + assert records[1]["headers"]["host"] == "census.cellxgene.cziscience.com" + assert "cellxgene-census-python" in records[1]["headers"]["user-agent"] + + +def test_download_w_proxy_fixture( + small_dataset_id: str, + collect_proxy_requests: Callable[[], list[dict]], + tmp_path: Path, + test_specific_useragent: str, +): + # Use of collect_proxy_requests forces test to use a proxy and will check headers of requests made via that proxy + adata_path = tmp_path / "adata.h5ad" + cellxgene_census.download_source_h5ad(small_dataset_id, adata_path.as_posix(), census_version="latest") + + records = collect_proxy_requests() + check_proxy_records( + records, + custom_user_agent=test_specific_useragent, + min_records=3, # Should request at least a json and the download + ) + + +def test_query_w_proxy_fixture(collect_proxy_requests: Callable[[], list[dict]]): + with cellxgene_census.open_soma(census_version="stable") as census: + _ = cellxgene_census.get_obs(census, "Mus musculus", coords=slice(100, 300)) + + records = collect_proxy_requests() + check_proxy_records( + records, + min_records=5, # some metadata requests, then a lot of request from tiledb + ) + + +def test_embedding_headers(collect_proxy_requests: Callable[[], list[dict]]): + import cellxgene_census.experimental + + CENSUS_VERSION = "2023-12-15" + + embeddings_metadata = cellxgene_census.experimental.get_all_available_embeddings(CENSUS_VERSION) + metadata = embeddings_metadata[0] + embedding_uri = ( + f"s3://cellxgene-contrib-public/contrib/cell-census/soma/{metadata['census_version']}/{metadata['id']}" + ) + _ = cellxgene_census.experimental.get_embedding( + CENSUS_VERSION, + embedding_uri=embedding_uri, + obs_soma_joinids=np.arange(100), + ) + + check_proxy_records(collect_proxy_requests()) + + +def test_dataloader_headers(collect_proxy_requests) -> None: + import cellxgene_census + from cellxgene_census.experimental.ml.pytorch import ExperimentDataPipe + + soma_experiment = cellxgene_census.open_soma(census_version="latest")["census_data"]["homo_sapiens"] + dp = ExperimentDataPipe( + soma_experiment, + measurement_name="RNA", + X_name="raw", + obs_column_names=["cell_type"], + shuffle=False, + ) + _ = next(iter(dp)) + + records = collect_proxy_requests() + check_proxy_records(records, min_records=5) From 309fc37cfcf89bf2b50b32bc26a846ab69b3ff13 Mon Sep 17 00:00:00 2001 From: Isaac Virshup Date: Wed, 3 Jul 2024 19:09:50 -0700 Subject: [PATCH 08/16] [python] Update docs for `get_obs`, `get_var` (#1170) * Initial set of changes * run pre-commit * add census_query_extract notebook * Use get_var in hvg notebook --- api/python/cellxgene_census/README.md | 14 +- .../comp_bio_embedding_exploration.ipynb | 6 +- .../comp_bio_explore_and_load_lung_data.ipynb | 9 +- .../comp_bio_summarize_axis_query.ipynb | 34 ++-- .../census_access_maintained_embeddings.ipynb | 10 +- .../api_demo/census_citation_generation.ipynb | 5 +- .../notebooks/api_demo/census_datasets.ipynb | 7 +- .../api_demo/census_duplicated_cells.ipynb | 150 +++++++++--------- .../notebooks/api_demo/census_embedding.ipynb | 12 +- .../api_demo/census_query_extract.ipynb | 35 ++-- .../experimental/highly_variable_genes.ipynb | 2 +- 11 files changed, 123 insertions(+), 161 deletions(-) diff --git a/api/python/cellxgene_census/README.md b/api/python/cellxgene_census/README.md index 4af92636c..a694717fe 100644 --- a/api/python/cellxgene_census/README.md +++ b/api/python/cellxgene_census/README.md @@ -23,19 +23,13 @@ import cellxgene_census with cellxgene_census.open_soma() as census: - # Reads SOMADataFrame as a slice - cell_metadata = census["census_data"]["homo_sapiens"].obs.read( + cell_metadata = cellxgene_census.get_obs( + census, + "homo_sapiens", value_filter = "sex == 'female' and cell_type in ['microglial cell', 'neuron']", column_names = ["assay", "cell_type", "tissue", "tissue_general", "suspension_type", "disease"] ) - - # Concatenates results to pyarrow.Table - cell_metadata = cell_metadata.concat() - - # Converts to pandas.DataFrame - cell_metadata = cell_metadata.to_pandas() - - print(cell_metadata) + cell_metadata ``` The output is a `pandas.DataFrame` with over 600K cells meeting our query criteria and the selected columns: diff --git a/api/python/notebooks/analysis_demo/comp_bio_embedding_exploration.ipynb b/api/python/notebooks/analysis_demo/comp_bio_embedding_exploration.ipynb index 8e872a8ce..e858c8df7 100644 --- a/api/python/notebooks/analysis_demo/comp_bio_embedding_exploration.ipynb +++ b/api/python/notebooks/analysis_demo/comp_bio_embedding_exploration.ipynb @@ -168,8 +168,7 @@ "# Let's find our cells of interest\n", "obs_value_filter = \"tissue_general=='eye' and is_primary_data == True\"\n", "\n", - "obs_df = census[\"census_data\"][EXPERIMENT_NAME].obs.read(value_filter=obs_value_filter, column_names=[\"soma_joinid\"])\n", - "obs_df = obs_df.concat().to_pandas()\n", + "obs_df = cellxgene_census.get_obs(census, EXPERIMENT_NAME, value_filter=obs_value_filter, column_names=[\"soma_joinid\"])\n", "\n", "print(obs_df.shape[0], \"cells in\", obs_value_filter)\n", "\n", @@ -578,9 +577,8 @@ "# Let's find our cells of interest\n", "obs_value_filter = \"tissue_general=='brain' and is_primary_data == True\"\n", "\n", - "obs_df = census[\"census_data\"][EXPERIMENT_NAME].obs.read(value_filter=obs_value_filter, column_names=[\"soma_joinid\"])\n", + "obs_df = cellxgene_census.get_obs(census, EXPERIMENT_NAME, value_filter=obs_value_filter, column_names=[\"soma_joinid\"])\n", "\n", - "obs_df = obs_df.concat().to_pandas()\n", "print(obs_df.shape[0], \"cells in\", obs_value_filter)\n", "\n", "# Let's subset to 150K\n", diff --git a/api/python/notebooks/analysis_demo/comp_bio_explore_and_load_lung_data.ipynb b/api/python/notebooks/analysis_demo/comp_bio_explore_and_load_lung_data.ipynb index b2d8cfdb2..952c643a1 100644 --- a/api/python/notebooks/analysis_demo/comp_bio_explore_and_load_lung_data.ipynb +++ b/api/python/notebooks/analysis_demo/comp_bio_explore_and_load_lung_data.ipynb @@ -603,11 +603,8 @@ } ], "source": [ - "lung_obs = (\n", - " census[\"census_data\"][\"homo_sapiens\"]\n", - " .obs.read(value_filter=\"tissue_general == 'lung' and is_primary_data == True\")\n", - " .concat()\n", - " .to_pandas()\n", + "lung_obs = cellxgene_census.get_obs(\n", + " census, \"homo_sapiens\", value_filter=\"tissue_general == 'lung' and is_primary_data == True\"\n", ")\n", "lung_obs" ] @@ -1612,7 +1609,7 @@ } ], "source": [ - "lung_var = census[\"census_data\"][\"homo_sapiens\"].ms[\"RNA\"].var.read().concat().to_pandas()\n", + "lung_var = cellxgene_census.get_var(census, \"homo_sapiens\")\n", "lung_var" ] }, diff --git a/api/python/notebooks/analysis_demo/comp_bio_summarize_axis_query.ipynb b/api/python/notebooks/analysis_demo/comp_bio_summarize_axis_query.ipynb index 04a6473eb..0d2e236e2 100644 --- a/api/python/notebooks/analysis_demo/comp_bio_summarize_axis_query.ipynb +++ b/api/python/notebooks/analysis_demo/comp_bio_summarize_axis_query.ipynb @@ -113,10 +113,8 @@ } ], "source": [ - "human = census[\"census_data\"][\"homo_sapiens\"]\n", - "\n", "# Read entire _obs_ into a pandas dataframe.\n", - "obs_df = human.obs.read(column_names=[\"cell_type_ontology_term_id\"]).concat().to_pandas()\n", + "obs_df = cellxgene_census.get_obs(census, \"homo_sapiens\", column_names=[\"cell_type_ontology_term_id\"])\n", "\n", "# Use Pandas API to find all unique values in the `cell_type_ontology_term_id` column.\n", "unique_cell_type_ontology_term_id = obs_df.cell_type_ontology_term_id.unique()\n", @@ -178,18 +176,15 @@ ], "source": [ "# Count cell_type occurrences for cells with tissue == 'lung'\n", - "human = census[\"census_data\"][\"homo_sapiens\"]\n", "\n", "# Read cell_type terms for cells which have a specific tissue term\n", "LUNG_TISSUE = \"UBERON:0002048\"\n", "\n", - "obs_df = (\n", - " human.obs.read(\n", - " column_names=[\"cell_type_ontology_term_id\"],\n", - " value_filter=f\"tissue_ontology_term_id == '{LUNG_TISSUE}'\",\n", - " )\n", - " .concat()\n", - " .to_pandas()\n", + "obs_df = cellxgene_census.get_obs(\n", + " census,\n", + " \"homo_sapiens\",\n", + " column_names=[\"cell_type_ontology_term_id\"],\n", + " value_filter=f\"tissue_ontology_term_id == '{LUNG_TISSUE}'\",\n", ")\n", "\n", "# Use Pandas API to find all unique values in the `cell_type_ontology_term_id` column.\n", @@ -251,17 +246,13 @@ ], "source": [ "# You can also do more complex queries, such as testing for inclusion in a list of values and \"and\" operations\n", - "human = census[\"census_data\"][\"homo_sapiens\"]\n", - "\n", "VENTRICLES = [\"UBERON:0002082\", \"UBERON:OOO2084\", \"UBERON:0002080\"]\n", "\n", - "obs_df = (\n", - " human.obs.read(\n", - " column_names=[\"cell_type_ontology_term_id\"],\n", - " value_filter=f\"tissue_ontology_term_id in {VENTRICLES} and is_primary_data == True\",\n", - " )\n", - " .concat()\n", - " .to_pandas()\n", + "obs_df = cellxgene_census.get_obs(\n", + " census,\n", + " \"homo_sapiens\",\n", + " column_names=[\"cell_type_ontology_term_id\"],\n", + " value_filter=f\"tissue_ontology_term_id in {VENTRICLES} and is_primary_data == True\",\n", ")\n", "\n", "# Use Pandas API to summarize\n", @@ -314,8 +305,7 @@ "]\n", "\n", "obs_df = {\n", - " name: experiment.obs.read(column_names=COLS_TO_QUERY).concat().to_pandas()\n", - " for name, experiment in census[\"census_data\"].items()\n", + " name: cellxgene_census.get_obs(census, name, column_names=COLS_TO_QUERY) for name in census[\"census_data\"].keys()\n", "}\n", "\n", "# Use Pandas API to summarize each organism\n", diff --git a/api/python/notebooks/api_demo/census_access_maintained_embeddings.ipynb b/api/python/notebooks/api_demo/census_access_maintained_embeddings.ipynb index c2f200e96..6d2d6c92f 100644 --- a/api/python/notebooks/api_demo/census_access_maintained_embeddings.ipynb +++ b/api/python/notebooks/api_demo/census_access_maintained_embeddings.ipynb @@ -422,12 +422,12 @@ "\n", "census = cellxgene_census.open_soma(census_version=census_version)\n", "\n", - "obs_df = census[\"census_data\"][experiment_name].obs.read(\n", + "obs_df = cellxgene_census.get_obs(\n", + " census,\n", + " experiment_name,\n", " value_filter=\"tissue_general == 'central nervous system'\",\n", " column_names=[\"soma_joinid\", \"cell_type\"],\n", - ")\n", - "\n", - "obs_df = obs_df.concat().to_pandas()" + ")" ] }, { @@ -445,7 +445,7 @@ "metadata": {}, "outputs": [], "source": [ - "metadata = get_embedding_metadata_by_name(\"scvi\", \"homo_sapiens\", census_version=census_version)\n", + "metadata = get_embedding_metadata_by_name(\"scvi\", experiment_name, census_version=census_version)\n", "embedding_uri = f\"s3://cellxgene-contrib-public/contrib/cell-census/soma/{metadata['census_version']}/{metadata['id']}\"\n", "embedding = get_embedding(metadata[\"census_version\"], embedding_uri, obs_df.soma_joinid.to_numpy())" ] diff --git a/api/python/notebooks/api_demo/census_citation_generation.ipynb b/api/python/notebooks/api_demo/census_citation_generation.ipynb index 9c50e3728..9845c7855 100644 --- a/api/python/notebooks/api_demo/census_citation_generation.ipynb +++ b/api/python/notebooks/api_demo/census_citation_generation.ipynb @@ -257,10 +257,9 @@ ], "source": [ "# Query cell metadata\n", - "cell_metadata = census[\"census_data\"][\"homo_sapiens\"].obs.read(\n", - " value_filter=\"tissue == 'cardiac atrium'\", column_names=[\"dataset_id\", \"cell_type\"]\n", + "cell_metadata = cellxgene_census.get_obs(\n", + " census, \"homo_sapiens\", value_filter=\"tissue == 'cardiac atrium'\", column_names=[\"dataset_id\", \"cell_type\"]\n", ")\n", - "cell_metadata = cell_metadata.concat().to_pandas()\n", "\n", "# Get a citation string for the slice\n", "slice_datasets = datasets[datasets[\"dataset_id\"].isin(cell_metadata[\"dataset_id\"])]\n", diff --git a/api/python/notebooks/api_demo/census_datasets.ipynb b/api/python/notebooks/api_demo/census_datasets.ipynb index da3202549..ec42b8b6f 100644 --- a/api/python/notebooks/api_demo/census_datasets.ipynb +++ b/api/python/notebooks/api_demo/census_datasets.ipynb @@ -350,13 +350,10 @@ ], "source": [ "# Count cells across all experiments\n", - "all_experiments = (\n", - " (organism_name, organism_experiment) for organism_name, organism_experiment in census[\"census_data\"].items()\n", - ")\n", "experiments_total_cells = 0\n", "print(\"Count by experiment:\")\n", - "for organism_name, organism_experiment in all_experiments:\n", - " num_cells = len(organism_experiment.obs.read(column_names=[\"soma_joinid\"]).concat().to_pandas())\n", + "for organism_name in census[\"census_data\"].keys():\n", + " num_cells = len(cellxgene_census.get_obs(census, organism_name, column_names=[\"soma_joinid\"]))\n", " print(f\"\\t{num_cells} cells in {organism_name}\")\n", " experiments_total_cells += num_cells\n", "\n", diff --git a/api/python/notebooks/api_demo/census_duplicated_cells.ipynb b/api/python/notebooks/api_demo/census_duplicated_cells.ipynb index 9b902430a..575a9119e 100644 --- a/api/python/notebooks/api_demo/census_duplicated_cells.ipynb +++ b/api/python/notebooks/api_demo/census_duplicated_cells.ipynb @@ -71,13 +71,14 @@ "import cellxgene_census\n", "\n", "tabula_muris_dataset_id = \"48b37086-25f7-4ecd-be66-f5bb378e3aea\"\n", - "\n", - "with cellxgene_census.open_soma() as census:\n", - " tabula_muris_obs = census[\"census_data\"][\"mus_musculus\"].obs.read(\n", - " value_filter=f\"dataset_id == '{tabula_muris_dataset_id}'\", column_names=[\"tissue\", \"is_primary_data\"]\n", - " )\n", - "\n", - " tabula_muris_obs = tabula_muris_obs.concat().to_pandas()" + "census = cellxgene_census.open_soma()\n", + "\n", + "tabula_muris_obs = cellxgene_census.get_obs(\n", + " census,\n", + " \"mus_musculus\",\n", + " value_filter=f\"dataset_id == '{tabula_muris_dataset_id}'\",\n", + " column_names=[\"tissue\", \"is_primary_data\"],\n", + ")" ] }, { @@ -165,12 +166,12 @@ "source": [ "tabula_muris_liver_dataset_id = \"6202a243-b713-4e12-9ced-c387f8483dea\"\n", "\n", - "with cellxgene_census.open_soma() as census:\n", - " tabula_muris_liver_obs = census[\"census_data\"][\"mus_musculus\"].obs.read(\n", - " value_filter=f\"dataset_id == '{tabula_muris_liver_dataset_id}'\", column_names=[\"tissue\", \"is_primary_data\"]\n", - " )\n", - "\n", - " tabula_muris_liver_obs = tabula_muris_liver_obs.concat().to_pandas()" + "tabula_muris_liver_obs = cellxgene_census.get_obs(\n", + " census,\n", + " \"mus_musculus\",\n", + " value_filter=f\"dataset_id == '{tabula_muris_liver_dataset_id}'\",\n", + " column_names=[\"tissue\", \"is_primary_data\"],\n", + ")" ] }, { @@ -256,15 +257,14 @@ } ], "source": [ - "with cellxgene_census.open_soma() as census:\n", - " nk_cells = census[\"census_data\"][\"homo_sapiens\"].obs.read(\n", - " value_filter=\"cell_type == 'natural killer cell' \"\n", - " \"and disease == 'COVID-19' \"\n", - " \"and sex == 'female'\"\n", - " \"and tissue_general == 'blood'\"\n", - " )\n", - "\n", - " nk_cells = nk_cells.concat().to_pandas()" + "nk_cells = cellxgene_census.get_obs(\n", + " census,\n", + " \"mus_musculus\",\n", + " value_filter=\"cell_type == 'natural killer cell' \"\n", + " \"and disease == 'COVID-19' \"\n", + " \"and sex == 'female'\"\n", + " \"and tissue_general == 'blood'\",\n", + ")" ] }, { @@ -323,16 +323,15 @@ } ], "source": [ - "with cellxgene_census.open_soma() as census:\n", - " nk_cells_primary = census[\"census_data\"][\"homo_sapiens\"].obs.read(\n", - " value_filter=\"cell_type == 'natural killer cell' \"\n", - " \"and disease == 'COVID-19' \"\n", - " \"and tissue_general == 'blood'\"\n", - " \"and sex == 'female'\"\n", - " \"and is_primary_data == True\"\n", - " )\n", - "\n", - " nk_cells_primary = nk_cells_primary.concat().to_pandas()" + "nk_cells_primary = cellxgene_census.get_obs(\n", + " census,\n", + " \"mus_musculus\",\n", + " value_filter=\"cell_type == 'natural killer cell' \"\n", + " \"and disease == 'COVID-19' \"\n", + " \"and tissue_general == 'blood'\"\n", + " \"and sex == 'female'\"\n", + " \"and is_primary_data == True\",\n", + ")" ] }, { @@ -397,16 +396,15 @@ } ], "source": [ - "with cellxgene_census.open_soma() as census:\n", - " adata = cellxgene_census.get_anndata(\n", - " census,\n", - " organism=\"Homo sapiens\",\n", - " var_value_filter=\"feature_name == 'AQP5'\",\n", - " obs_value_filter=\"cell_type == 'natural killer cell' \"\n", - " \"and disease == 'COVID-19' \"\n", - " \"and sex == 'female'\"\n", - " \"and tissue_general == 'blood'\",\n", - " )" + "adata = cellxgene_census.get_anndata(\n", + " census,\n", + " organism=\"Homo sapiens\",\n", + " var_value_filter=\"feature_name == 'AQP5'\",\n", + " obs_value_filter=\"cell_type == 'natural killer cell' \"\n", + " \"and disease == 'COVID-19' \"\n", + " \"and sex == 'female'\"\n", + " \"and tissue_general == 'blood'\",\n", + ")" ] }, { @@ -465,17 +463,16 @@ } ], "source": [ - "with cellxgene_census.open_soma() as census:\n", - " adata_primary = cellxgene_census.get_anndata(\n", - " census,\n", - " organism=\"Homo sapiens\",\n", - " var_value_filter=\"feature_name == 'AQP5'\",\n", - " obs_value_filter=\"cell_type == 'natural killer cell' \"\n", - " \"and disease == 'COVID-19' \"\n", - " \"and sex == 'female' \"\n", - " \"and tissue_general == 'blood'\"\n", - " \"and is_primary_data == True\",\n", - " )" + "adata_primary = cellxgene_census.get_anndata(\n", + " census,\n", + " organism=\"Homo sapiens\",\n", + " var_value_filter=\"feature_name == 'AQP5'\",\n", + " obs_value_filter=\"cell_type == 'natural killer cell' \"\n", + " \"and disease == 'COVID-19' \"\n", + " \"and sex == 'female' \"\n", + " \"and tissue_general == 'blood'\"\n", + " \"and is_primary_data == True\",\n", + ")" ] }, { @@ -556,30 +553,29 @@ "source": [ "import tiledbsoma\n", "\n", - "with cellxgene_census.open_soma() as census:\n", - " human = census[\"census_data\"][\"homo_sapiens\"]\n", - "\n", - " # initialize lazy query\n", - " query = human.axis_query(\n", - " measurement_name=\"RNA\",\n", - " obs_query=tiledbsoma.AxisQuery(\n", - " value_filter=\"cell_type == 'natural killer cell' \"\n", - " \"and disease == 'COVID-19' \"\n", - " \"and tissue_general == 'blood' \"\n", - " \"and sex == 'female' \"\n", - " \"and is_primary_data == True\"\n", - " ),\n", - " )\n", - "\n", - " # get iterator for X\n", - " iterator = query.X(\"raw\").tables()\n", - "\n", - " # iterate in chunks\n", - " for chunk in iterator:\n", - " print(chunk)\n", - "\n", - " # since this is a demo we stop right away\n", - " break" + "human = census[\"census_data\"][\"homo_sapiens\"]\n", + "\n", + "# initialize lazy query\n", + "query = human.axis_query(\n", + " measurement_name=\"RNA\",\n", + " obs_query=tiledbsoma.AxisQuery(\n", + " value_filter=\"cell_type == 'natural killer cell' \"\n", + " \"and disease == 'COVID-19' \"\n", + " \"and tissue_general == 'blood' \"\n", + " \"and sex == 'female' \"\n", + " \"and is_primary_data == True\"\n", + " ),\n", + ")\n", + "\n", + "# get iterator for X\n", + "iterator = query.X(\"raw\").tables()\n", + "\n", + "# iterate in chunks\n", + "for chunk in iterator:\n", + " print(chunk)\n", + "\n", + " # since this is a demo we stop right away\n", + " break" ] } ], diff --git a/api/python/notebooks/api_demo/census_embedding.ipynb b/api/python/notebooks/api_demo/census_embedding.ipynb index 0d714380e..5f5f85a5c 100644 --- a/api/python/notebooks/api_demo/census_embedding.ipynb +++ b/api/python/notebooks/api_demo/census_embedding.ipynb @@ -59,9 +59,9 @@ "source": [ "from cellxgene_census.experimental import get_all_available_embeddings\n", "\n", - "census_version = \"2023-12-15\"\n", + "CENSUS_VERSION = \"2023-12-15\"\n", "\n", - "for e in get_all_available_embeddings(census_version):\n", + "for e in get_all_available_embeddings(CENSUS_VERSION):\n", " print(f\"{e['embedding_name']:15} {e['experiment_name']:15} {e['data_type']:15}\")" ] }, @@ -450,12 +450,12 @@ "source": [ "census = cellxgene_census.open_soma(census_version=CENSUS_VERSION)\n", "\n", - "obs_df = census[\"census_data\"][EXPERIMENT_NAME].obs.read(\n", + "obs_df = cellxgene_census.get_obs(\n", + " census,\n", + " EXPERIMENT_NAME,\n", " value_filter=\"tissue_general == 'exocrine gland'\",\n", " column_names=[\"soma_joinid\", \"cell_type\"],\n", - ")\n", - "\n", - "obs_df = obs_df.concat().to_pandas()" + ")" ] }, { diff --git a/api/python/notebooks/api_demo/census_query_extract.ipynb b/api/python/notebooks/api_demo/census_query_extract.ipynb index 3cdaf5b14..2f6f9791a 100644 --- a/api/python/notebooks/api_demo/census_query_extract.ipynb +++ b/api/python/notebooks/api_demo/census_query_extract.ipynb @@ -383,7 +383,7 @@ "source": [ "## Querying cell metadata (obs)\n", "\n", - "The human gene metadata of the Census, for RNA assays, is located at `census[\"census_data\"][\"homo_sapiens\"].obs`. This is a `SOMADataFrame` and as such it can be materialized as a `pandas.DataFrame` via the methods `read().concat().to_pandas()`. \n", + "The human gene metadata of the Census, for RNA assays, is located at `census[\"census_data\"][\"homo_sapiens\"].obs`. This is a `SOMADataFrame` and as such it can be materialized as a `pandas.DataFrame` via the methods `read().concat().to_pandas()`. See also, the helper function `cellxgene_census.get_obs` which removes some boiler plate.\n", "\n", "The mouse cell metadata is at `census[\"census_data\"][\"mus_musculus\"].obs`.\n", "\n", @@ -526,7 +526,7 @@ } ], "source": [ - "sex_cell_metadata = census[\"census_data\"][\"homo_sapiens\"].obs.read(column_names=[\"sex\"]).concat().to_pandas()\n", + "sex_cell_metadata = cellxgene_census.get_obs(census, \"homo_sapiens\", column_names=[\"sex\"])\n", "\n", "sex_cell_metadata.drop_duplicates()" ] @@ -980,9 +980,7 @@ } ], "source": [ - "cell_metadata_all_unknown_sex = (\n", - " census[\"census_data\"][\"homo_sapiens\"].obs.read(value_filter=\"sex == 'unknown'\").concat().to_pandas()\n", - ")\n", + "cell_metadata_all_unknown_sex = cellxgene_census.get_obs(census, \"homo_sapiens\", value_filter=\"sex == 'unknown'\")\n", "\n", "cell_metadata_all_unknown_sex" ] @@ -1033,14 +1031,11 @@ } ], "source": [ - "cell_metadata_b_cell = (\n", - " census[\"census_data\"][\"homo_sapiens\"]\n", - " .obs.read(\n", - " value_filter=\"cell_type == 'B cell' and tissue_general == 'lung' and is_primary_data==True\",\n", - " column_names=[\"disease\"],\n", - " )\n", - " .concat()\n", - " .to_pandas()\n", + "cell_metadata_b_cell = cellxgene_census.get_obs(\n", + " census,\n", + " \"homo_sapiens\",\n", + " value_filter=\"cell_type == 'B cell' and tissue_general == 'lung' and is_primary_data==True\",\n", + " column_names=[\"disease\"],\n", ")\n", "\n", "cell_metadata_b_cell.value_counts()" @@ -1164,15 +1159,11 @@ } ], "source": [ - "gene_metadata = (\n", - " census[\"census_data\"][\"homo_sapiens\"]\n", - " .ms[\"RNA\"]\n", - " .var.read(\n", - " value_filter=\"feature_id in ['ENSG00000161798', 'ENSG00000188229']\",\n", - " column_names=[\"feature_name\", \"feature_length\"],\n", - " )\n", - " .concat()\n", - " .to_pandas()\n", + "gene_metadata = cellxgene_census.get_var(\n", + " census,\n", + " \"homo_sapiens\",\n", + " value_filter=\"feature_id in ['ENSG00000161798', 'ENSG00000188229']\",\n", + " column_names=[\"feature_name\", \"feature_length\"],\n", ")\n", "\n", "gene_metadata" diff --git a/api/python/notebooks/experimental/highly_variable_genes.ipynb b/api/python/notebooks/experimental/highly_variable_genes.ipynb index f5a69c031..bd0a218d7 100644 --- a/api/python/notebooks/experimental/highly_variable_genes.ipynb +++ b/api/python/notebooks/experimental/highly_variable_genes.ipynb @@ -258,7 +258,7 @@ " )\n", "\n", " # while the Census is open, also grab the var dataframe for the mouse\n", - " var_df = census[\"census_data\"][\"mus_musculus\"].ms[\"RNA\"].var.read().concat().to_pandas()\n", + " var_df = cellxgene_census.get_var(census, \"mus_musculus\")\n", "\n", "hvgs_df" ] From 1b24d78180a8038122c0820eb9da171e29853fce Mon Sep 17 00:00:00 2001 From: Mike Lin Date: Fri, 5 Jul 2024 12:24:47 -1000 Subject: [PATCH 09/16] [python] Geneformer updates for July 2024 LTS (#961) changes to our Geneformer API and workflows accumulated for the new LTS: * Run tokenization/fine-tuning/forward-pass WDLs on AWS HealthOmics instead of Batch * Update the upstream Geneformer version * Add new `special_token` flag * Use a gene ID consolidation mapping, with modifications to the sparse math to implement * Add WDL inputs for slight variations on embeddings we want to try * Update ontologies for cell subclasses --- .github/workflows/py-unittests.yml | 3 + .../scripts/requirements-dev.txt | 1 - .../ml/huggingface/cell_dataset_builder.py | 1 + .../ml/huggingface/geneformer_tokenizer.py | 110 +++++++++--- .../ml/huggingface/test_geneformer.py | 5 +- tools/models/geneformer/Dockerfile | 46 ++--- tools/models/geneformer/README.md | 53 +++--- tools/models/geneformer/buildspec.yml | 15 ++ .../geneformer/finetune-geneformer.config.yml | 3 + .../models/geneformer/finetune-geneformer.py | 2 + .../generate-geneformer-embeddings.py | 26 +-- .../geneformer/helpers/cl.v2024-04-05.owl.gz | Bin 0 -> 3008238 bytes .../geneformer/helpers/ontology_mapper.py | 35 ++-- .../helpers/uberon.v2024-03-22.owl.gz | Bin 0 -> 5747559 bytes .../prepare-census-geneformer-dataset.py | 159 ++++++++++++------ .../geneformer/wdl/finetune_geneformer.wdl | 2 + .../geneformer/wdl/generate_embeddings.wdl | 27 +-- .../geneformer/wdl/prepare_datasets.wdl | 10 +- 18 files changed, 323 insertions(+), 175 deletions(-) create mode 100644 tools/models/geneformer/buildspec.yml create mode 100644 tools/models/geneformer/helpers/cl.v2024-04-05.owl.gz create mode 100644 tools/models/geneformer/helpers/uberon.v2024-03-22.owl.gz diff --git a/.github/workflows/py-unittests.yml b/.github/workflows/py-unittests.yml index 404902c38..ad68fd0d8 100644 --- a/.github/workflows/py-unittests.yml +++ b/.github/workflows/py-unittests.yml @@ -44,6 +44,9 @@ jobs: pip install --use-pep517 accumulation-tree # Geneformer dependency needs --use-pep517 for Cython GIT_CLONE_PROTECTION_ACTIVE=false pip install -r ./api/python/cellxgene_census/scripts/requirements-dev.txt pip install -e './api/python/cellxgene_census/[experimental]' + - name: Install Geneformer (python >=3.10 only) + run: pip install git+https://huggingface.co/ctheodoris/Geneformer@471eefc + if: matrix.python-version != '3.8' && matrix.python-version != '3.9' - name: Report Dependency Versions run: pip list - name: Test with pytest (API, main tests) diff --git a/api/python/cellxgene_census/scripts/requirements-dev.txt b/api/python/cellxgene_census/scripts/requirements-dev.txt index e80038f06..7fab730ad 100644 --- a/api/python/cellxgene_census/scripts/requirements-dev.txt +++ b/api/python/cellxgene_census/scripts/requirements-dev.txt @@ -5,6 +5,5 @@ twine coverage nbqa transformers[torch] -git+https://huggingface.co/ctheodoris/Geneformer@8df5dc1 owlready2 proxy.py diff --git a/api/python/cellxgene_census/src/cellxgene_census/experimental/ml/huggingface/cell_dataset_builder.py b/api/python/cellxgene_census/src/cellxgene_census/experimental/ml/huggingface/cell_dataset_builder.py index 6b274e8fd..07d2212c8 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/experimental/ml/huggingface/cell_dataset_builder.py +++ b/api/python/cellxgene_census/src/cellxgene_census/experimental/ml/huggingface/cell_dataset_builder.py @@ -66,6 +66,7 @@ def gen() -> Generator[Dict[str, Any], None, None]: self.X(self.layer_name).blockwise(axis=0, reindex_disable_on_axis=[1], size=self.block_size).scipy() ): assert isinstance(Xblock, scipy.sparse.csr_matrix) + assert Xblock.shape[0] == len(block_cell_joinids) for i, cell_joinid in enumerate(block_cell_joinids): yield self.cell_item(cell_joinid, Xblock.getrow(i)) diff --git a/api/python/cellxgene_census/src/cellxgene_census/experimental/ml/huggingface/geneformer_tokenizer.py b/api/python/cellxgene_census/src/cellxgene_census/experimental/ml/huggingface/geneformer_tokenizer.py index 3c8310fe1..1da99bf02 100644 --- a/api/python/cellxgene_census/src/cellxgene_census/experimental/ml/huggingface/geneformer_tokenizer.py +++ b/api/python/cellxgene_census/src/cellxgene_census/experimental/ml/huggingface/geneformer_tokenizer.py @@ -1,5 +1,5 @@ import pickle -from typing import Any, Dict, Optional, Sequence, Set +from typing import Any, Dict, List, Optional, Sequence, Set import numpy as np import numpy.typing as npt @@ -14,7 +14,7 @@ class GeneformerTokenizer(CellDatasetBuilder): cell in CELLxGENE Census ExperimentAxisQuery results (human). This class requires the Geneformer package to be installed separately with: - `pip install git+https://huggingface.co/ctheodoris/Geneformer@8df5dc1` + `pip install git+https://huggingface.co/ctheodoris/Geneformer@471eefc` Example usage: @@ -44,11 +44,18 @@ class GeneformerTokenizer(CellDatasetBuilder): obs_column_names: Set[str] max_input_tokens: int - - # set of gene soma_joinids corresponding to genes modeled by Geneformer: - model_gene_ids: npt.NDArray[np.int64] - model_gene_tokens: npt.NDArray[np.int64] # token for each model_gene_id - model_gene_medians: npt.NDArray[np.float64] # float for each model_gene_id + special_token: bool + + # Newer versions of Geneformer has a consolidated gene list (gene_mapping_file), meaning the + # counts for one or more Census genes are to be summed to get the count for one Geneformer + # gene. model_gene_map is a sparse binary matrix to map a cell vector (or multi-cell matrix) of + # Census gene counts onto Geneformer gene counts. model_gene_map[i,j] is 1 iff the i'th Census + # gene count contributes to the j'th Geneformer gene count. + model_gene_map: scipy.sparse.coo_matrix + model_gene_tokens: npt.NDArray[np.int64] # Geneformer token for each column of model_gene_map + model_gene_medians: npt.NDArray[np.float64] # float for each column of model_gene_map + model_cls_token: Optional[np.int64] = None + model_sep_token: Optional[np.int64] = None def __init__( self, @@ -57,25 +64,33 @@ def __init__( obs_column_names: Optional[Sequence[str]] = None, obs_attributes: Optional[Sequence[str]] = None, max_input_tokens: int = 2048, + special_token: bool = False, token_dictionary_file: str = "", gene_median_file: str = "", + gene_mapping_file: str = "", **kwargs: Any, ) -> None: - """- `experiment`: Census Experiment to query + """Initialize GeneformerTokenizer. + + Args: + - `experiment`: Census Experiment to query - `obs_query`: obs AxisQuery defining the set of Census cells to process (default all) - `obs_column_names`: obs dataframe columns (cell metadata) to propagate into attributes of each Dataset item - `max_input_tokens`: maximum length of Geneformer input token sequence (default 2048) + - `special_token`: whether to affix separator tokens to the sequence (default False) - `token_dictionary_file`, `gene_median_file`: pickle files supplying the mapping of Ensembl human gene IDs onto Geneformer token numbers and median expression values. By default, these will be loaded from the Geneformer package. + - `gene_mapping_file`: optional pickle file with mapping for Census gene IDs to model's """ if obs_attributes: # old name of obs_column_names obs_column_names = obs_attributes self.max_input_tokens = max_input_tokens + self.special_token = special_token self.obs_column_names = set(obs_column_names) if obs_column_names else set() - self._load_geneformer_data(experiment, token_dictionary_file, gene_median_file) + self._load_geneformer_data(experiment, token_dictionary_file, gene_median_file, gene_mapping_file) super().__init__( experiment, measurement_name="RNA", @@ -88,6 +103,7 @@ def _load_geneformer_data( experiment: tiledbsoma.Experiment, token_dictionary_file: str, gene_median_file: str, + gene_mapping_file: str, ) -> None: """Load (1) the experiment's genes dataframe and (2) Geneformer's static data files for gene tokens and median expression; then, intersect them to compute @@ -95,7 +111,13 @@ def _load_geneformer_data( """ # TODO: this work could be reused for all queries on this experiment - genes_df = experiment.ms["RNA"].var.read(column_names=["soma_joinid", "feature_id"]).concat().to_pandas() + genes_df = ( + experiment.ms["RNA"] + .var.read(column_names=["soma_joinid", "feature_id"]) + .concat() + .to_pandas() + .set_index("soma_joinid") + ) if not (token_dictionary_file and gene_median_file): try: @@ -104,7 +126,7 @@ def _load_geneformer_data( # pyproject.toml can't express Geneformer git+https dependency raise ImportError( "Please install Geneformer with: " - "pip install git+https://huggingface.co/ctheodoris/Geneformer@8df5dc1" + "pip install git+https://huggingface.co/ctheodoris/Geneformer@471eefc" ) from None if not token_dictionary_file: token_dictionary_file = geneformer.tokenizer.TOKEN_DICTIONARY_FILE @@ -115,27 +137,47 @@ def _load_geneformer_data( with open(gene_median_file, "rb") as f: gene_median_dict = pickle.load(f) + gene_mapping = None + if gene_mapping_file: + with open(gene_mapping_file, "rb") as f: + gene_mapping = pickle.load(f) + # compute model_gene_{ids,tokens,medians} by joining genes_df with Geneformer's # dicts - model_gene_ids = [] - model_gene_tokens = [] - model_gene_medians = [] + map_data = [] + map_i = [] + map_j = [] + model_gene_id_by_ensg: Dict[str, int] = {} + model_gene_count = 0 + model_gene_tokens: List[np.int64] = [] + model_gene_medians: List[np.float64] = [] for gene_id, row in genes_df.iterrows(): ensg = row["feature_id"] # ENSG... gene id, which keys Geneformer's dicts + if gene_mapping is not None: + ensg = gene_mapping.get(ensg, ensg) if ensg in gene_token_dict: - model_gene_ids.append(gene_id) - model_gene_tokens.append(gene_token_dict[ensg]) - model_gene_medians.append(gene_median_dict[ensg]) - self.model_gene_ids = np.array(model_gene_ids, dtype=np.int64) + if ensg not in model_gene_id_by_ensg: + model_gene_id_by_ensg[ensg] = model_gene_count + model_gene_count += 1 + model_gene_tokens.append(gene_token_dict[ensg]) + model_gene_medians.append(gene_median_dict[ensg]) + map_data.append(1) + map_i.append(gene_id) + map_j.append(model_gene_id_by_ensg[ensg]) + + self.model_gene_map = scipy.sparse.coo_matrix( + (map_data, (map_i, map_j)), shape=(genes_df.index.max() + 1, model_gene_count), dtype=bool + ) self.model_gene_tokens = np.array(model_gene_tokens, dtype=np.int64) self.model_gene_medians = np.array(model_gene_medians, dtype=np.float64) - assert len(np.unique(self.model_gene_ids)) == len(self.model_gene_ids) assert len(np.unique(self.model_gene_tokens)) == len(self.model_gene_tokens) assert np.all(self.model_gene_medians > 0) # Geneformer models protein-coding and miRNA genes, so the intersection should - # be somewhere a little north of 20K. - assert len(self.model_gene_ids) > 20_000 + # be north of 18K. + assert ( + model_gene_count > 18_000 + ), f"Mismatch between Census gene IDs and Geneformer token dicts (only {model_gene_count} common genes)" # Precompute a vector by which we'll multiply each cell's expression vector. # The denominator normalizes by Geneformer's median expression values. @@ -143,6 +185,10 @@ def _load_geneformer_data( # affect the rank order, but is probably intended to help with numerical precision. self.model_gene_medians_factor = 10_000.0 / self.model_gene_medians + if self.special_token: + self.model_cls_token = gene_token_dict[""] + self.model_sep_token = gene_token_dict[""] + def __enter__(self) -> "GeneformerTokenizer": super().__enter__() # On context entry, load the necessary cell metadata (obs_df) @@ -156,21 +202,29 @@ def cell_item(self, cell_joinid: int, cell_Xrow: scipy.sparse.csr_matrix) -> Dic """Given the expression vector for one cell, compute the Dataset item providing the Geneformer inputs (token sequence and metadata). """ - # project cell_Xrow onto model_gene_ids and normalize by row sum. - # notice we divide by the total count of the complete row (not only of the projected + # Apply model_gene_map to cell_Xrow and normalize with row sum & gene medians. + # Notice we divide by the total count of the complete row (not only of the projected # values); this follows Geneformer's internal tokenizer. - model_counts = cell_Xrow[:, self.model_gene_ids].multiply(1.0 / cell_Xrow.sum()) - assert isinstance(model_counts, scipy.sparse.csr_matrix), type(model_counts) - # assert len(model_counts.data) == np.count_nonzero(model_counts.data) - model_expr = model_counts.multiply(self.model_gene_medians_factor) + model_expr = (cell_Xrow * self.model_gene_map).multiply(self.model_gene_medians_factor / cell_Xrow.sum()) assert isinstance(model_expr, scipy.sparse.coo_matrix), type(model_expr) - # assert len(model_expr.data) == np.count_nonzero(model_expr.data) + assert model_expr.shape == (1, self.model_gene_map.shape[1]) # figure the resulting tokens in descending order of model_expr # (use sparse model_expr.{col,data} to naturally exclude undetected genes) token_order = model_expr.col[np.argsort(-model_expr.data)[: self.max_input_tokens]] input_ids = self.model_gene_tokens[token_order] + if self.special_token: + # affix special tokens, dropping the last two gene tokens if necessary + if len(input_ids) == self.max_input_tokens: + input_ids = input_ids[:-1] + assert self.model_cls_token is not None + input_ids = np.insert(input_ids, 0, self.model_cls_token) + if len(input_ids) == self.max_input_tokens: + input_ids = input_ids[:-1] + assert self.model_sep_token is not None + input_ids = np.append(input_ids, self.model_sep_token) + ans = {"input_ids": input_ids, "length": len(input_ids)} # add the requested obs attributes for attr in self.obs_column_names: diff --git a/api/python/cellxgene_census/tests/experimental/ml/huggingface/test_geneformer.py b/api/python/cellxgene_census/tests/experimental/ml/huggingface/test_geneformer.py index 626fcddb2..e95992a14 100644 --- a/api/python/cellxgene_census/tests/experimental/ml/huggingface/test_geneformer.py +++ b/api/python/cellxgene_census/tests/experimental/ml/huggingface/test_geneformer.py @@ -1,3 +1,5 @@ +import sys + import datasets import pytest import tiledbsoma @@ -67,7 +69,7 @@ def test_GeneformerTokenizer_correctness(tmpdir: Path) -> None: ad.write_h5ad(h5ad_dir.join("tokenizeme.h5ad")) # run geneformer.TranscriptomeTokenizer to get "true" tokenizations # see: https://huggingface.co/ctheodoris/Geneformer/blob/main/geneformer/tokenizer.py - TranscriptomeTokenizer({}).tokenize_data(h5ad_dir, tmpdir, "tk", file_format="h5ad") + TranscriptomeTokenizer({}).tokenize_data(h5ad_dir, str(tmpdir), "tk", file_format="h5ad") true_tokens = [it["input_ids"] for it in datasets.load_from_disk(tmpdir.join("tk.dataset"))] # check GeneformerTokenizer sequences against geneformer.TranscriptomeTokenizer's @@ -88,6 +90,7 @@ def test_GeneformerTokenizer_correctness(tmpdir: Path) -> None: assert identical / len(cell_ids) >= EXACT_THRESHOLD +@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") @pytest.mark.experimental @pytest.mark.live_corpus def test_GeneformerTokenizer_docstring_example() -> None: diff --git a/tools/models/geneformer/Dockerfile b/tools/models/geneformer/Dockerfile index b1f485bc4..cd9a67114 100644 --- a/tools/models/geneformer/Dockerfile +++ b/tools/models/geneformer/Dockerfile @@ -1,41 +1,49 @@ # Builds a docker image with: -# - PyTorch+CUDA +# - CUDA+PyTorch # - Geneformer # - cellxgene_census # - our Census-Geneformer training scripts -FROM nvcr.io/nvidia/pytorch:23.10-py3 +FROM nvcr.io/nvidia/cuda:11.8.0-runtime-ubuntu22.04 -# Set the tiledbsoma version used to write the embeddings SparseNDArray, to ensure -# compatibility with the Census embeddings curator -ARG EMBEDDINGS_TILEDBSOMA_VERSION=1.4.4 -ARG GENEFORMER_VERSION=8df5dc1 - -RUN apt update && apt install -y python3-venv git-lfs pigz +RUN apt update && apt install -y build-essential python3-pip python3-venv git-lfs pigz libcurl4-openssl-dev RUN git lfs install -ENV GIT_SSL_NO_VERIFY=true -RUN pip install \ - transformers[torch] \ - "cellxgene_census[experimental] @ git+https://github.com/chanzuckerberg/cellxgene-census.git#subdirectory=api/python/cellxgene_census" \ - git+https://huggingface.co/ctheodoris/Geneformer@${GENEFORMER_VERSION} -RUN pip install owlready2 boto3 +ENV GIT_SSL_NO_VERIFY=true +RUN pip install --upgrade pip setuptools setuptools_scm +RUN pip install torch torchdata --index-url https://download.pytorch.org/whl/cu118 + # ^^^ match the base image CUDA version! +RUN pip install owlready2 boto3 transformers[torch] # workaround for unknown problem blocking `import geneformer`: # https://github.com/microsoft/TaskMatrix/issues/116#issuecomment-1565431850 RUN pip uninstall -y transformer-engine -# smoke test -RUN python3 -c 'import geneformer; import cellxgene_census; cellxgene_census.open_soma()' + +# Set the tiledbsoma version used to write the embeddings SparseNDArray, to ensure +# compatibility with the Census embeddings curator +ARG EMBEDDINGS_TILEDBSOMA_VERSION=1.9.5 +ARG CELLXGENE_CENSUS_VERSION=main +ARG GENEFORMER_VERSION=471eefc RUN mkdir /census-geneformer WORKDIR /census-geneformer -# clone Geneformer separately to get LFS files +RUN git clone https://github.com/chanzuckerberg/cellxgene-census.git \ + && git -C cellxgene-census checkout ${CELLXGENE_CENSUS_VERSION} +RUN pip install cellxgene-census/api/python/cellxgene_census RUN git clone --recursive https://huggingface.co/ctheodoris/Geneformer \ && git -C Geneformer checkout ${GENEFORMER_VERSION} +RUN pip install -e Geneformer -# prepare a venv with tiledbsoma ${EMBEDDINGS_TILEDBSOMA_VERSION} +# smoke test +RUN python3 -c 'import geneformer; import cellxgene_census; from cellxgene_census.experimental.ml.huggingface import GeneformerTokenizer; cellxgene_census.open_soma()' + +# prepare a venv with pinned tiledbsoma ${EMBEDDINGS_TILEDBSOMA_VERSION}, which our embeddings +# generation step will use to output a TileDB array compatible with the Census embeddings curator. RUN python3 -m venv --system-site-packages embeddings_tiledbsoma_venv && \ . embeddings_tiledbsoma_venv/bin/activate && \ pip install tiledbsoma==${EMBEDDINGS_TILEDBSOMA_VERSION} -COPY *.py . COPY helpers ./helpers +COPY *.py ./ COPY finetune-geneformer.config.yml . + +# FIXME: eliminate once model is published in Geneformer repo +COPY gf-95m/ ./gf-95m/ diff --git a/tools/models/geneformer/README.md b/tools/models/geneformer/README.md index c6f45065a..c360fb382 100644 --- a/tools/models/geneformer/README.md +++ b/tools/models/geneformer/README.md @@ -12,48 +12,53 @@ The `Dockerfile` provides the recipe for the docker image used by the WDLs, whic ## Example invocations -Using a [miniwdl-aws](https://github.com/miniwdl-ext/miniwdl-aws) deployment with suitable GPU instance types enabled on the underlying AWS Batch compute environment, and assuming the docker image has been built and pushed to a suitable repository like ECR (tagged `$DOCKER_TAG`). +Using [miniwdl-omics-run](https://github.com/miniwdl-ext/miniwdl-omics-run) for the Amazon HealthOmics workflow service, and assuming the docker image has been built and pushed to a suitable repository like ECR (tagged `$DOCKER_TAG`). Preparing a tokenized training dataset with 2,500 primary cells per human cell type: ```bash -miniwdl-aws-submit --verbose --follow --workflow-queue miniwdl-workflow \ - wdl/prepare_datasets.wdl docker=$DOCKER_TAG \ - census_version=2023-10-23 N=2500 sampling_column=cell_type output_name=2500_per_cell_type \ - --s3upload s3://MYBUCKET/geneformer/datasets/2500_per_cell_type/ +miniwdl-omics-run wdl/prepare_datasets.wdl \ + docker=$DOCKER_TAG \ + census_version=s3://cellxgene-census-public-us-west-2/cell-census/2023-12-15/soma/ \ + N=2500 sampling_column=cell_type output_name=2500_per_cell_type \ + --role poweromics --output-uri s3://MYBUCKET/geneformer/datasets/ ``` -And a tokenized dataset for all of Census (371GiB!): +And a tokenized dataset for all of Census (>300GiB, sharded): ```bash -miniwdl-aws-submit --verbose --follow --workflow-queue miniwdl-workflow \ - wdl/prepare_datasets.wdl docker=$DOCKER_TAG \ - census_version=2023-10-23 output_name=census-2023-10-23 value_filter='is_primary_data==True or is_primary_data==False' \ - --s3upload s3://MYBUCKET/geneformer/datasets/census-2023-10-23/ +miniwdl-omics-run wdl/prepare_datasets.wdl \ + docker=$DOCKER_TAG \ + census_version=s3://cellxgene-census-public-us-west-2/cell-census/2024-05-20/soma/ \ + value_filter='is_primary_data==True or is_primary_data==False' \ + output_name=2024-05-20 shards=256 \ + --role poweromics --output-uri s3://MYBUCKET/geneformer/datasets/ ``` +(We set `census_version` to the SOMACollection URI because the HealthOmics workers don't have internet access to the Census release directory endpoint.) + Fine-tuning for 8 epochs (takes ~36h on g5.8xlarge): ```bash -MINIWDL__AWS__GPU_VALUE=8 \ -MINIWDL__AWS__CONTAINER_PROPERTIES='{"linuxParameters":{"sharedMemorySize":4096}}' \ -miniwdl-aws-submit --verbose --follow --workflow-queue miniwdl-workflow \ - wdl/finetune_geneformer.wdl docker=$DOCKER_TAG \ +miniwdl-omics-run wdl/finetune_geneformer.wdl \ + docker=$DOCKER_TAG \ dataset=s3://MYBUCKET/geneformer/datasets/2500_per_cell_type/dataset/2500_per_cell_type \ epochs=8 output_name=2500_per_cell_type_8epochs \ - --s3upload s3://MYBUCKET/geneformer/models/2500_per_cell_type_8epochs/ + --role poweromics --output-uri s3://MYBUCKET/geneformer/models/ ``` Generating cell embeddings (takes 8-12h on up to 256 g5.2xlarge, generates 130GiB `tiledbsoma.SparseNDArray` on S3): ```bash -MINIWDL__SCHEDULER__CALL_CONCURRENCY=256 \ -MINIWDL__AWS__SUBMIT_PERIOD=60 \ -miniwdl-aws-submit --verbose --follow --workflow-queue miniwdl-workflow \ - wdl/generate_embeddings.wdl docker=$DOCKER_TAG \ - emb_layer=0 \ - dataset=s3://MYBUCKET/geneformer/datasets/census-2023-10-23/dataset/census-2023-10-23 \ - model=s3://MYBUCKET/geneformer/models/2500_per_cell_type_8epochs/model/2500_per_cell_type_8epochs \ - output_uri=s3://MYBUCKET/geneformer/embs/census-2023-10-23 parts=256 \ - --s3upload s3://MYBUCKET/geneformer/embs +seq 0 255 \ + | xargs -n 1 printf 'dataset_shards=s3://MYBUCKET/geneformer/datasets/census-2024-05-20/shard-%03d/\n' \ + | xargs -n 9999 miniwdl-omics-run \ + --role poweromics --output-uri s3://MYBUCKET/geneformer/embs \ + wdl/generate_embeddings.wdl \ + docker=$DOCKER_TAG \ + emb_layer=0 model_type=Pretrained \ + model=s3://MYBUCKET/geneformer/gf-95m/fine_tuned_model/ \ + output_uri=s3_//MYBUCKET/geneformer/embs/$(date '+%s')/census-2024-05-20/ ``` + +(The `s3_//MYBUCKET` is a workaround for the workflow service rejecting our submission if the specified S3 output folder doesn't yet exist; this workflow has TileDB create it.) diff --git a/tools/models/geneformer/buildspec.yml b/tools/models/geneformer/buildspec.yml new file mode 100644 index 000000000..0c439d650 --- /dev/null +++ b/tools/models/geneformer/buildspec.yml @@ -0,0 +1,15 @@ +# This CodeBuild spec is used to build the docker image and push it to ECR. +# (The image is >10GB so can be painful to push from outside AWS.) +version: 0.2 + +phases: + pre_build: + commands: + - aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 699936264352.dkr.ecr.us-west-2.amazonaws.com + build: + commands: + - aws s3 cp s3://mlin-census-scratch/geneformer/gf-95m/ tools/models/geneformer/gf-95m/ --recursive + - docker build -t 699936264352.dkr.ecr.us-west-2.amazonaws.com/omics:census-geneformer --build-arg CELLXGENE_CENSUS_VERSION=$CODEBUILD_RESOLVED_SOURCE_VERSION tools/models/geneformer + post_build: + commands: + - docker push 699936264352.dkr.ecr.us-west-2.amazonaws.com/omics:census-geneformer diff --git a/tools/models/geneformer/finetune-geneformer.config.yml b/tools/models/geneformer/finetune-geneformer.config.yml index cf72418d2..734fa3b1a 100644 --- a/tools/models/geneformer/finetune-geneformer.config.yml +++ b/tools/models/geneformer/finetune-geneformer.config.yml @@ -3,6 +3,9 @@ label_feature: cell_subclass # Specific labels to exclude from training and evaluation label_blocklist: - unknown + - abnormal cell + - animal cell + - eukaryotic cell # Also exclude labels with too few examples label_min_examples: 10 # Fraction of the input Dataset to hold out for evaluation diff --git a/tools/models/geneformer/finetune-geneformer.py b/tools/models/geneformer/finetune-geneformer.py index 13427706e..cf3f3af5a 100644 --- a/tools/models/geneformer/finetune-geneformer.py +++ b/tools/models/geneformer/finetune-geneformer.py @@ -10,6 +10,7 @@ from collections import Counter import pandas as pd +import torch import yaml from datasets import Dataset from geneformer import DataCollatorForCellClassification @@ -22,6 +23,7 @@ def main(argv): + assert torch.cuda.is_available(), "CUDA is not available" args = parse_arguments(argv) if os.path.exists(args.model_out): logger.error("output directory already exists: " + args.model_out) diff --git a/tools/models/geneformer/generate-geneformer-embeddings.py b/tools/models/geneformer/generate-geneformer-embeddings.py index e3adbc589..225f07b16 100755 --- a/tools/models/geneformer/generate-geneformer-embeddings.py +++ b/tools/models/geneformer/generate-geneformer-embeddings.py @@ -8,7 +8,8 @@ import tempfile import geneformer -from datasets import Dataset, disable_progress_bar +import torch +from datasets import disable_progress_bar from transformers import BertConfig logging.basicConfig(level=logging.INFO, format="%(asctime)s %(module)s [%(levelname)s] %(message)s") @@ -17,6 +18,7 @@ def main(argv): + assert torch.cuda.is_available(), "CUDA is not available" args = parse_arguments(argv) tiledbsoma_context = None @@ -56,7 +58,6 @@ def main(argv): with tempfile.TemporaryDirectory() as scratch_dir: # prepare the dataset, taking only one shard of it if so instructed - dataset_path = prepare_dataset(args.dataset, args.part, args.parts, scratch_dir) logger.info("Extracting embeddings...") extractor = geneformer.EmbExtractor( model_type=args.model_type, @@ -70,7 +71,7 @@ def main(argv): # see https://huggingface.co/ctheodoris/Geneformer/blob/main/geneformer/emb_extractor.py embs_df = extractor.extract_embs( model_directory=args.model, - input_data_file=dataset_path, + input_data_file=args.dataset, # the method always writes out a .csv file which we discard (since it also returns the # embeddings data frame) output_directory=scratch_dir, @@ -124,8 +125,6 @@ def parse_arguments(argv): help="dataset features to copy into output dataframe (comma-separated)", ) parser.add_argument("--batch-size", type=int, default=16, help="batch size") - parser.add_argument("--part", type=int, help="process only one shard of the data (zero-based index)") - parser.add_argument("--parts", type=int, help="required with --part") parser.add_argument( "--tiledbsoma", action="store_true", @@ -141,26 +140,9 @@ def parse_arguments(argv): if "soma_joinid" not in args.features: args.features.append("soma_joinid") - if args.part is not None: - if not (args.part >= 0 and args.parts is not None and args.parts > args.part): - parser.error("--part must be nonnegative and less than --parts") - logger.info("arguments: " + str(vars(args))) return args -def prepare_dataset(dataset_dir, part, parts, spool_dir): - dataset = Dataset.load_from_disk(dataset_dir) - logger.info(f"dataset (full): {dataset}") - if part is None: - return dataset_dir - dataset = dataset.shard(num_shards=parts, index=part, contiguous=True) - # spool the desired part of the dataset (since EmbExtractor takes a directory) - logger.info(f"dataset part: {dataset}") - part_dir = os.path.join(spool_dir, "dataset_part") - dataset.save_to_disk(part_dir) - return part_dir - - if __name__ == "__main__": sys.exit(main(sys.argv)) diff --git a/tools/models/geneformer/helpers/cl.v2024-04-05.owl.gz b/tools/models/geneformer/helpers/cl.v2024-04-05.owl.gz new file mode 100644 index 0000000000000000000000000000000000000000..f4c633e6b92c5b6bf4204d4d9aa00b08dacb92eb GIT binary patch literal 3008238 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