From 2e825ba205ae2b2ad4e03ae660b6e8257100828a Mon Sep 17 00:00:00 2001 From: nayib-jose-gloria Date: Fri, 15 Sep 2023 11:06:28 -0400 Subject: [PATCH 1/7] feat: require tissue_type field and add new validation rules for tissue_ontology_term_id and cell_type_ontology_term_id --- .../schema_definitions/schema_definition.yaml | 52 ++++++++++++++---- .../tests/fixtures/examples_validate.py | 8 ++- .../fixtures/h5ads/example_invalid_CL.h5ad | Bin 573648 -> 575672 bytes .../tests/fixtures/h5ads/example_valid.h5ad | Bin 573536 -> 575888 bytes .../tests/test_schema_compliance.py | 29 +++++++--- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/cellxgene_schema_cli/cellxgene_schema/schema_definitions/schema_definition.yaml b/cellxgene_schema_cli/cellxgene_schema/schema_definitions/schema_definition.yaml index 2ffd5aba1..bd5cac351 100644 --- a/cellxgene_schema_cli/cellxgene_schema/schema_definitions/schema_definition.yaml +++ b/cellxgene_schema_cli/cellxgene_schema/schema_definitions/schema_definition.yaml @@ -103,6 +103,10 @@ components: curie_constraints: ontologies: - CL + forbidden: + - CL:0000255 + - CL:0000257 + - CL:0000548 add_labels: - type: curie @@ -163,15 +167,35 @@ components: to_column: sex tissue_ontology_term_id: type: curie - curie_constraints: - ontologies: - - UBERON - - CL - suffixes: - UBERON: - - " (organoid)" - CL: - - " (cell culture)" + dependencies: + - + # If tissue_type is tissue OR organoid + rule: "tissue_type == 'tissue' | tissue_type == 'organoid'" + error_message_suffix: >- + When 'tissue_type' is 'tissue' or 'organoid', + 'tissue_ontology_term_id' MUST be a child term id of 'UBERON:0001062' (anatomical entity). + type: curie + curie_constraints: + ontologies: + - UBERON + ancestors: + UBERON: + - UBERON:0001062 + - + # If tissue_type is cell culture + rule: "tissue_type == 'cell culture'" + error_message_suffix: >- + When 'tissue_type' is 'cell culture', 'tissue_ontology_term_id' MUST be a CL term + and it can not be 'CL:0000255' (eukaryotic cell), 'CL:0000257' (Eumycetozoan cell), + nor 'CL:0000548' (animal cell). + type: curie + curie_constraints: + ontologies: + - CL + forbidden: + - CL:0000255 + - CL:0000257 + - CL:0000548 add_labels: - type: curie @@ -527,4 +551,12 @@ components: when 'assay_ontology_term_id' is EFO:0009919 enum: - "cell" - - "nucleus" \ No newline at end of file + - "nucleus" + tissue_type: + type: categorical + error_message_suffix: >- + Only 'cell culture', 'organoid', or 'tissue' are allowed. + enum: + - "cell culture" + - "organoid" + - "tissue" \ No newline at end of file diff --git a/cellxgene_schema_cli/tests/fixtures/examples_validate.py b/cellxgene_schema_cli/tests/fixtures/examples_validate.py index e38c81fde..fe39afa50 100644 --- a/cellxgene_schema_cli/tests/fixtures/examples_validate.py +++ b/cellxgene_schema_cli/tests/fixtures/examples_validate.py @@ -41,6 +41,7 @@ "NCBITaxon:9606", "PATO:0000383", "UBERON:0002048", + "tissue", True, "HANCESTRO:0575", "HsapDv:0000003", @@ -53,7 +54,8 @@ "PATO:0000461", "NCBITaxon:10090", "unknown", - "CL:0000192 (cell culture)", + "CL:0000192", + "cell culture", False, "na", "MmusDv:0000003", @@ -69,6 +71,7 @@ "organism_ontology_term_id", "sex_ontology_term_id", "tissue_ontology_term_id", + "tissue_type", "is_primary_data", "self_reported_ethnicity_ontology_term_id", "development_stage_ontology_term_id", @@ -79,6 +82,7 @@ good_obs.loc[:, ["donor_id"]] = good_obs.astype("category") good_obs.loc[:, ["suspension_type"]] = good_obs.astype("category") +good_obs.loc[:, ["tissue_type"]] = good_obs.astype("category") # Expected obs, this is what the obs above should look like after adding the necessary columns with the validator, # these columns are defined in the schema @@ -100,7 +104,7 @@ "normal", "Mus musculus", "unknown", - "smooth muscle cell (cell culture)", + "smooth muscle cell", "na", "Theiler stage 01", ], diff --git a/cellxgene_schema_cli/tests/fixtures/h5ads/example_invalid_CL.h5ad b/cellxgene_schema_cli/tests/fixtures/h5ads/example_invalid_CL.h5ad index e219b619bc853349e8ac6edc1138edddc71b9f25..53ea1b189db7e596efe157f00475ab28832b829c 100644 GIT binary patch delta 29858 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 delta 29226 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 rh$Ox4)^$e}nMW%P$V#K%vLKY^X&fC+QF+WgF Date: Fri, 15 Sep 2023 11:08:23 -0400 Subject: [PATCH 2/7] validate raw.var and raw.X dimension consistency if raw exists --- .../cellxgene_schema/validate.py | 7 +++++++ cellxgene_schema_cli/tests/test_validate.py | 19 +++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/cellxgene_schema_cli/cellxgene_schema/validate.py b/cellxgene_schema_cli/cellxgene_schema/validate.py index 62d987101..964c1982c 100644 --- a/cellxgene_schema_cli/cellxgene_schema/validate.py +++ b/cellxgene_schema_cli/cellxgene_schema/validate.py @@ -800,6 +800,13 @@ def _validate_seurat_convertibility(self): ) self.is_seurat_convertible = False + if self.adata.raw and self.adata.raw.X.shape[1] != self.adata.raw.var.shape[0]: + self.warnings.append( + f"This dataset cannot be converted to the .rds (Seurat v4) format. " + f"There is a mismatch in the number of variables in the raw matrix and the raw var key-indexed " + f"variables." + ) + self.is_seurat_convertible = False def _validate_embedding_dict(self): """ diff --git a/cellxgene_schema_cli/tests/test_validate.py b/cellxgene_schema_cli/tests/test_validate.py index 0c71f6fa6..447fe295c 100644 --- a/cellxgene_schema_cli/tests/test_validate.py +++ b/cellxgene_schema_cli/tests/test_validate.py @@ -54,16 +54,16 @@ def test_schema_definition(self): self.assertTrue("type" in self.schema_def["components"]["obs"]["columns"][i]) if i == "curie": self.assertIsInstance( - self.schema_def["components"]["obs"]["columns"][i]["curie_constrains"], + self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"], dict, ) self.assertIsInstance( - self.schema_def["components"]["obs"]["columns"][i]["curie_constrains"]["ontolgies"], + self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"]["ontolgies"], list, ) # Check that the allowed ontologies are in the ontology checker - for ontology_name in self.schema_def["components"]["obs"]["columns"][i]["curie_constrains"][ + for ontology_name in self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"][ "ontolgies" ]: self.assertTrue(self.OntologyChecker.is_valid_ontology(ontology_name)) @@ -281,8 +281,10 @@ def test__validate_with_h5ad_invalid_and_without_labels(self): class TestSeuratConvertibility(unittest.TestCase): - def validation_helper(self, matrix): + def validation_helper(self, matrix, raw=None): data = anndata.AnnData(X=matrix, obs=good_obs, uns=good_uns, obsm=good_obsm, var=good_var) + if raw: + data.raw = raw self.validator: Validator = Validator() self.validator._set_schema_def() self.validator.schema_def["max_size_for_seurat"] = 2**3 - 1 # Reduce size required to fail (faster tests) @@ -319,3 +321,12 @@ def test_determine_seurat_convertibility(self): self.validator._validate_seurat_convertibility() self.assertTrue(len(self.validator.warnings) == 0) self.assertTrue(self.validator.is_seurat_convertible) + + # h5ad where raw matrix variable count != length of raw var variables array is not Seurat-convertible + matrix = sparse.csr_matrix(np.zeros([good_obs.shape[0], good_var.shape[0]], dtype=np.float32)) + raw = anndata.AnnData(X=matrix, var=good_var) + raw.var.drop('ENSSASG00005000004', axis=0, inplace=True) + self.validation_helper(matrix, raw) + self.validator._validate_seurat_convertibility() + self.assertTrue(len(self.validator.warnings) == 1) + self.assertFalse(self.validator.is_seurat_convertible) From 5ab4e57a7ec878ed18d86e9047ab52db79cd92e3 Mon Sep 17 00:00:00 2001 From: nayib-jose-gloria Date: Fri, 15 Sep 2023 13:07:52 -0400 Subject: [PATCH 3/7] =?UTF-8?q?Bump=20version:=204.0.0-rc.0=20=E2=86=92=20?= =?UTF-8?q?4.0.0-rc.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cellxgene_schema_cli/cellxgene_schema/__init__.py | 2 +- cellxgene_schema_cli/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cellxgene_schema_cli/cellxgene_schema/__init__.py b/cellxgene_schema_cli/cellxgene_schema/__init__.py index cb467ae1d..14c14eb36 100644 --- a/cellxgene_schema_cli/cellxgene_schema/__init__.py +++ b/cellxgene_schema_cli/cellxgene_schema/__init__.py @@ -1 +1 @@ -__version__ = "4.0.0-rc.0" +__version__ = "__version__ = "4.0.0-rc.0"" diff --git a/cellxgene_schema_cli/setup.py b/cellxgene_schema_cli/setup.py index e51d24064..95f2c1573 100644 --- a/cellxgene_schema_cli/setup.py +++ b/cellxgene_schema_cli/setup.py @@ -5,7 +5,7 @@ setup( name="cellxgene-schema", - version="4.0.0-rc.0", + version="version="4.0.0-rc.0"", url="https://github.com/chanzuckerberg/single-cell-curation", license="MIT", author="Chan Zuckerberg Initiative", From 2b85dc8a02dab19b21a964cfb97edcb30b57dfef Mon Sep 17 00:00:00 2001 From: nayib-jose-gloria Date: Fri, 15 Sep 2023 13:08:46 -0400 Subject: [PATCH 4/7] add tests + simplify suffix write-labels + validation now that its not allowed --- .../schema_definitions/schema_definition.yaml | 2 - .../cellxgene_schema/validate.py | 47 +----- .../cellxgene_schema/write_labels.py | 24 +-- .../tests/test_schema_compliance.py | 138 +++++++++++++++--- cellxgene_schema_cli/tests/test_validate.py | 40 +++-- 5 files changed, 150 insertions(+), 101 deletions(-) diff --git a/cellxgene_schema_cli/cellxgene_schema/schema_definitions/schema_definition.yaml b/cellxgene_schema_cli/cellxgene_schema/schema_definitions/schema_definition.yaml index bd5cac351..caf2ae618 100644 --- a/cellxgene_schema_cli/cellxgene_schema/schema_definitions/schema_definition.yaml +++ b/cellxgene_schema_cli/cellxgene_schema/schema_definitions/schema_definition.yaml @@ -554,8 +554,6 @@ components: - "nucleus" tissue_type: type: categorical - error_message_suffix: >- - Only 'cell culture', 'organoid', or 'tissue' are allowed. enum: - "cell culture" - "organoid" diff --git a/cellxgene_schema_cli/cellxgene_schema/validate.py b/cellxgene_schema_cli/cellxgene_schema/validate.py index 964c1982c..c3a3720d8 100644 --- a/cellxgene_schema_cli/cellxgene_schema/validate.py +++ b/cellxgene_schema_cli/cellxgene_schema/validate.py @@ -43,35 +43,6 @@ def __init__(self, ignore_labels=False): # Matrix (e.g., X, raw.X, ...) number non-zero cache self.number_non_zero = dict() - @staticmethod - def _curie_remove_suffix(term_id: str, suffix_def: dict) -> Tuple[str, str]: - """ - Remove suffix from a curie term id, if none present return it unmodified - - :param str term_id: the curie term id to validate - :param dict{str: list[str], ...} suffix_def: dictionary whose keys are ontology term ids and values - are list of allowed suffixes - - :rtype Tuple[str, str] - :return the term_id with suffixed stripped, and the suffix - """ - - id_suffix = "" - - for ontology_name, suffixes in suffix_def.items(): - for suffix in suffixes: - suffix = suffix.replace("(", r"\(") - suffix = suffix.replace(")", r"\)") - search_results = re.search(r"%s$" % suffix, term_id) - if search_results: - stripped_term_id = re.sub(r"%s$" % suffix, "", term_id) - if ONTOLOGY_CHECKER.is_valid_term_id(ontology_name, stripped_term_id): - id_suffix = search_results.group(0) - - return stripped_term_id, id_suffix - - return term_id, id_suffix - def _validate_encoding_version(self): import h5py @@ -228,7 +199,7 @@ def _validate_curie(self, term_id: str, column_name: str, curie_constraints: dic # If there are forbidden terms if "forbidden" in curie_constraints and term_id in curie_constraints["forbidden"]: - self.errors.append(f"'{term_id}' in '{column_name}' is not allowed'.") + self.errors.append(f"'{term_id}' in '{column_name}' is not allowed.") return # If NA is found in allowed ontologies, it means only exceptions should be found. If no exceptions were found @@ -237,10 +208,6 @@ def _validate_curie(self, term_id: str, column_name: str, curie_constraints: dic self.errors.append(f"'{term_id}' in '{column_name}' is not a valid value of '{column_name}'.") return - # Check if there are any allowed suffixes and remove them if needed - if "suffixes" in curie_constraints: - term_id, suffix = self._curie_remove_suffix(term_id, curie_constraints["suffixes"]) - # Check that term id belongs to allowed ontologies self._validate_curie_ontology(term_id, column_name, curie_constraints["ontologies"]) @@ -436,15 +403,9 @@ def _validate_column(self, column: pd.Series, column_name: str, df_name: str, co self.errors.append(f"Column '{column_name}' in dataframe '{df_name}' must not contain NaN values.") return - if "curie_constraints" not in column_def: - raise ValueError(f"Corrupt schema definition, no 'curie_constraints' were found for '{column_name}'") - if "ontologies" not in column_def["curie_constraints"]: - raise ValueError( - f"allowed 'ontologies' must be specified under 'curie constraints' for '{column_name}'" - ) - - for term_id in column.drop_duplicates(): - self._validate_curie(term_id, column_name, column_def["curie_constraints"]) + if "curie_constraints" in column_def: + for term_id in column.drop_duplicates(): + self._validate_curie(term_id, column_name, column_def["curie_constraints"]) # Add error suffix to errors found here if "error_message_suffix" in column_def: diff --git a/cellxgene_schema_cli/cellxgene_schema/write_labels.py b/cellxgene_schema_cli/cellxgene_schema/write_labels.py index f558c3b22..380bd6534 100644 --- a/cellxgene_schema_cli/cellxgene_schema/write_labels.py +++ b/cellxgene_schema_cli/cellxgene_schema/write_labels.py @@ -122,31 +122,21 @@ def _get_mapping_dict_curie(self, ids: List[str], curie_constraints: dict) -> Di mapping_dict = {} allowed_ontologies = curie_constraints["ontologies"] - # Remove any suffixes if any - # original_ids will have untouched ids which will be used for mapping - # id_suffixes will save suffixes if any, these will be used to append to labels - # ids will have the ids without suffixes - original_ids = ids.copy() - id_suffixes = [""] * len(ids) - - if "suffixes" in curie_constraints: - for i in range(len(ids)): - ids[i], id_suffixes[i] = Validator._curie_remove_suffix(ids[i], curie_constraints["suffixes"]) - - for original_id, id, id_suffix in zip(original_ids, ids, id_suffixes): + # Map term_ids to their human-readable ontology labels + for term_id in ids: # If there are exceptions the label should be the same as the id - if "exceptions" in curie_constraints and original_id in curie_constraints["exceptions"]: - mapping_dict[original_id] = original_id + if "exceptions" in curie_constraints and term_id in curie_constraints["exceptions"]: + mapping_dict[term_id] = term_id continue for ontology_name in allowed_ontologies: if ontology_name == "NA": continue - if ONTOLOGY_CHECKER.is_valid_term_id(ontology_name, id): - mapping_dict[original_id] = ONTOLOGY_CHECKER.get_term_label(ontology_name, id) + id_suffix + if ONTOLOGY_CHECKER.is_valid_term_id(ontology_name, term_id): + mapping_dict[term_id] = ONTOLOGY_CHECKER.get_term_label(ontology_name, term_id) # Check that all ids got a mapping. All ids should be found if adata was validated - for id in original_ids: + for id in ids: if id not in mapping_dict: raise ValueError(f"Add labels error: Unable to get label for '{id}'") diff --git a/cellxgene_schema_cli/tests/test_schema_compliance.py b/cellxgene_schema_cli/tests/test_schema_compliance.py index 56af22d6c..2e67f307a 100644 --- a/cellxgene_schema_cli/tests/test_schema_compliance.py +++ b/cellxgene_schema_cli/tests/test_schema_compliance.py @@ -281,16 +281,31 @@ def test_assay_ontology_term_id(self): def test_cell_type_ontology_term_id(self): """ - cell_type_ontology_term_id categorical with str categories. This MUST be a CL term. + cell_type_ontology_term_id categorical with str categories. This MUST be a CL term, and must NOT match forbidden + columns defined in schema """ # Not a valid term - self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "cell_type_ontology_term_id"] = "EFO:0000001" - self.validator.validate_adata() - self.assertEqual( - self.validator.errors, - ["ERROR: 'EFO:0000001' in 'cell_type_ontology_term_id' is not a valid " "ontology term id of 'CL'."], - ) + with self.subTest(forbidden_term="EFO:0000001"): + self.validator.adata.obs.loc[ + self.validator.adata.obs.index[0], "cell_type_ontology_term_id" + ] = "EFO:0000001" + self.validator.validate_adata() + self.assertEqual( + self.validator.errors, + ["ERROR: 'EFO:0000001' in 'cell_type_ontology_term_id' is not a valid " "ontology term id of 'CL'."], + ) + + for term in self.validator.schema_def["components"]["obs"]["columns"]["cell_type_ontology_term_id"][ + "curie_constraints" + ]["forbidden"]: + with self.subTest(forbidden_term=term): + self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "cell_type_ontology_term_id"] = term + self.validator.validate_adata() + self.assertEqual( + self.validator.errors, + [f"ERROR: '{term}' in 'cell_type_ontology_term_id' is not allowed."], + ) def test_development_stage_ontology_term_id_human(self): """ @@ -377,7 +392,7 @@ def test_development_stage_ontology_term_id_all_species(self): self.assertEqual( self.validator.errors, [ - "ERROR: 'UBERON:0000071' in 'development_stage_ontology_term_id' is not allowed'. When " + "ERROR: 'UBERON:0000071' in 'development_stage_ontology_term_id' is not allowed. When " "'organism_ontology_term_id' is not 'NCBITaxon:10090' " "nor 'NCBITaxon:9606', 'development_stage_ontology_term_id' MUST be a child term id of " "'UBERON:0000105' excluding 'UBERON:0000071', or unknown.", @@ -506,23 +521,54 @@ def test_tissue_ontology_term_id_base(self): def test_tissue_ontology_term_id_cell_culture(self): """ - Cell Culture - must not accept suffixes like "(cell culture)" + Cell Culture - must be a valid CL term other than forbidden columns in schema definition. Can NOT include + suffixes. """ - - self.validator.adata.obs.loc[ - self.validator.adata.obs.index[0], "tissue_ontology_term_id" - ] = "CL:0000057 (cell culture)" self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "tissue_type"] = "cell culture" - self.validator.validate_adata() - self.assertEqual( - self.validator.errors, - [ - "ERROR: 'CL:0000057 (cell culture)' in 'tissue_ontology_term_id' is not a valid ontology term id of " - "'CL'. When 'tissue_type' is 'cell culture', 'tissue_ontology_term_id' MUST be a CL term " - "and it can not be 'CL:0000255' (eukaryotic cell), 'CL:0000257' (Eumycetozoan cell), " - "nor 'CL:0000548' (animal cell)." - ], - ) + + with self.subTest(case="error, suffix in term ID"): + self.validator.adata.obs.loc[ + self.validator.adata.obs.index[0], "tissue_ontology_term_id" + ] = "CL:0000057 (cell culture)" + self.validator.validate_adata() + self.assertEqual( + self.validator.errors, + [ + "ERROR: 'CL:0000057 (cell culture)' in 'tissue_ontology_term_id' is not a valid ontology term id " + "of 'CL'. When 'tissue_type' is 'cell culture', 'tissue_ontology_term_id' MUST be a CL term " + "and it can not be 'CL:0000255' (eukaryotic cell), 'CL:0000257' (Eumycetozoan cell), " + "nor 'CL:0000548' (animal cell)." + ], + ) + + with self.subTest(case="error, not a CL term"): + self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "tissue_ontology_term_id"] = "EFO:0000001" + self.validator.validate_adata() + self.assertEqual( + self.validator.errors, + [ + "ERROR: 'EFO:0000001' in 'tissue_ontology_term_id' is not a valid ontology term id of " + "'CL'. When 'tissue_type' is 'cell culture', 'tissue_ontology_term_id' MUST be a CL term " + "and it can not be 'CL:0000255' (eukaryotic cell), 'CL:0000257' (Eumycetozoan cell), " + "nor 'CL:0000548' (animal cell)." + ], + ) + + for term in self.validator.schema_def["components"]["obs"]["columns"]["cell_type_ontology_term_id"][ + "curie_constraints" + ]["forbidden"]: + with self.subTest(case="error", forbidden_term=term): + self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "tissue_ontology_term_id"] = term + self.validator.validate_adata() + self.assertEqual( + self.validator.errors, + [ + f"ERROR: '{term}' in 'tissue_ontology_term_id' is not allowed. When 'tissue_type' is " + f"'cell culture', 'tissue_ontology_term_id' MUST be a CL term " + "and it can not be 'CL:0000255' (eukaryotic cell), 'CL:0000257' (Eumycetozoan cell), " + "nor 'CL:0000548' (animal cell)." + ], + ) def test_tissue_ontology_term_id_organoid(self): """ @@ -544,6 +590,52 @@ def test_tissue_ontology_term_id_organoid(self): ], ) + def test_tissue_ontology_term_id_child_of_anatomical_entity(self): + """ + Tissue ontology term ID must be a CHILD TERM of 'UBERON:0001062' (anatomical entity) if tissue_type is + organoid or tissue. + """ + self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "tissue_ontology_term_id"] = "UBERON:0001062" + with self.subTest(tissue_type="tissue"): + self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "tissue_type"] = "tissue" + self.validator.validate_adata() + self.assertEqual( + self.validator.errors, + [ + "ERROR: 'UBERON:0001062' in 'tissue_ontology_term_id' is not a child term id of " + "'[['UBERON:0001062']]'. When 'tissue_type' is 'tissue' or 'organoid', 'tissue_ontology_term_id' " + "MUST be a child term id of 'UBERON:0001062' (anatomical entity)." + ], + ) + + with self.subTest(tissue_type="organoid"): + self.validator.adata.obs.tissue_type = self.validator.adata.obs.tissue_type.cat.add_categories(["organoid"]) + self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "tissue_type"] = "organoid" + self.validator.validate_adata() + self.assertEqual( + self.validator.errors, + [ + "ERROR: 'UBERON:0001062' in 'tissue_ontology_term_id' is not a child term id of " + "'[['UBERON:0001062']]'. When 'tissue_type' is 'tissue' or 'organoid', 'tissue_ontology_term_id' " + "MUST be a child term id of 'UBERON:0001062' (anatomical entity)." + ], + ) + + def test_tissue_type(self): + """ + tissue_type must be one of 'cell culture', 'tissue', or 'organoid' + """ + self.validator.adata.obs.tissue_type = self.validator.adata.obs.tissue_type.cat.add_categories(["organ"]) + self.validator.adata.obs.loc[self.validator.adata.obs.index[0], "tissue_type"] = "organ" + self.validator.validate_adata() + self.assertEqual( + self.validator.errors, + [ + "ERROR: Column 'tissue_type' in dataframe 'obs' contains invalid values " + "'['organ']'. Values must be one of ['cell culture', 'organoid', 'tissue']" + ], + ) + def test_sex_ontology_term_id(self): """ sex_ontology_term_id categorical with str categories. diff --git a/cellxgene_schema_cli/tests/test_validate.py b/cellxgene_schema_cli/tests/test_validate.py index 447fe295c..36df2231e 100644 --- a/cellxgene_schema_cli/tests/test_validate.py +++ b/cellxgene_schema_cli/tests/test_validate.py @@ -52,21 +52,29 @@ def test_schema_definition(self): # Check that any columns in obs that are "curie" have "curie_constraints" and "ontologies" under the constraints for i in self.schema_def["components"]["obs"]["columns"]: self.assertTrue("type" in self.schema_def["components"]["obs"]["columns"][i]) - if i == "curie": - self.assertIsInstance( - self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"], - dict, - ) - self.assertIsInstance( - self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"]["ontolgies"], - list, - ) - - # Check that the allowed ontologies are in the ontology checker - for ontology_name in self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"][ - "ontolgies" - ]: - self.assertTrue(self.OntologyChecker.is_valid_ontology(ontology_name)) + if self.schema_def["components"]["obs"]["columns"][i]["type"] == "curie": + if "curie_constraints" in self.schema_def["components"]["obs"]["columns"][i]: + self.assertIsInstance( + self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"], + dict, + ) + self.assertIsInstance( + self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"]["ontologies"], + list, + ) + + # Check that the allowed ontologies are in the ontology checker or 'NA' (special case) + for ontology_name in self.schema_def["components"]["obs"]["columns"][i]["curie_constraints"][ + "ontologies" + ]: + if ontology_name != "NA": + self.assertTrue(self.OntologyChecker.is_valid_ontology(ontology_name)) + else: + # if no curie_constraints in top-level for type curie, assert that 'dependencies' list exists + self.assertIsInstance( + self.schema_def["components"]["obs"]["columns"][i]["dependencies"], + list, + ) def test_validate_ontology_good(self): self.validator._validate_curie("CL:0000066", self.column_name, self.curie_constraints) @@ -325,7 +333,7 @@ def test_determine_seurat_convertibility(self): # h5ad where raw matrix variable count != length of raw var variables array is not Seurat-convertible matrix = sparse.csr_matrix(np.zeros([good_obs.shape[0], good_var.shape[0]], dtype=np.float32)) raw = anndata.AnnData(X=matrix, var=good_var) - raw.var.drop('ENSSASG00005000004', axis=0, inplace=True) + raw.var.drop("ENSSASG00005000004", axis=0, inplace=True) self.validation_helper(matrix, raw) self.validator._validate_seurat_convertibility() self.assertTrue(len(self.validator.warnings) == 1) From d696720c52ea8406923c73cdd8b3126bbe497ee4 Mon Sep 17 00:00:00 2001 From: nayib-jose-gloria Date: Fri, 15 Sep 2023 13:09:48 -0400 Subject: [PATCH 5/7] =?UTF-8?q?Bump=20version:=204.0.0-rc.0=20=E2=86=92=20?= =?UTF-8?q?4.0.0-rc.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cellxgene_schema_cli/cellxgene_schema/__init__.py | 2 +- cellxgene_schema_cli/setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5a2e12934..d391347f1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.0.0-rc.0 +current_version = 4.0.0-rc.1 commit = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?:-(?Prc)\.(?P\d+))? serialize = diff --git a/cellxgene_schema_cli/cellxgene_schema/__init__.py b/cellxgene_schema_cli/cellxgene_schema/__init__.py index 14c14eb36..abb8475c3 100644 --- a/cellxgene_schema_cli/cellxgene_schema/__init__.py +++ b/cellxgene_schema_cli/cellxgene_schema/__init__.py @@ -1 +1 @@ -__version__ = "__version__ = "4.0.0-rc.0"" +__version__ = "__version__ = "4.0.0-rc.1"" diff --git a/cellxgene_schema_cli/setup.py b/cellxgene_schema_cli/setup.py index 95f2c1573..db2a3135e 100644 --- a/cellxgene_schema_cli/setup.py +++ b/cellxgene_schema_cli/setup.py @@ -5,7 +5,7 @@ setup( name="cellxgene-schema", - version="version="4.0.0-rc.0"", + version="version="4.0.0-rc.1"", url="https://github.com/chanzuckerberg/single-cell-curation", license="MIT", author="Chan Zuckerberg Initiative", From 8203af30ac70fd7e7e9c744297edd2bff5588868 Mon Sep 17 00:00:00 2001 From: nayib-jose-gloria Date: Fri, 15 Sep 2023 13:14:17 -0400 Subject: [PATCH 6/7] fix versions --- cellxgene_schema_cli/cellxgene_schema/__init__.py | 2 +- cellxgene_schema_cli/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cellxgene_schema_cli/cellxgene_schema/__init__.py b/cellxgene_schema_cli/cellxgene_schema/__init__.py index abb8475c3..6ecb6d70d 100644 --- a/cellxgene_schema_cli/cellxgene_schema/__init__.py +++ b/cellxgene_schema_cli/cellxgene_schema/__init__.py @@ -1 +1 @@ -__version__ = "__version__ = "4.0.0-rc.1"" +__version__ = "4.0.0-rc.1" diff --git a/cellxgene_schema_cli/setup.py b/cellxgene_schema_cli/setup.py index db2a3135e..91937e0d2 100644 --- a/cellxgene_schema_cli/setup.py +++ b/cellxgene_schema_cli/setup.py @@ -5,7 +5,7 @@ setup( name="cellxgene-schema", - version="version="4.0.0-rc.1"", + version="4.0.0-rc.1", url="https://github.com/chanzuckerberg/single-cell-curation", license="MIT", author="Chan Zuckerberg Initiative", From fb5384ec642d267bb650fe51ac91280ea10c9f25 Mon Sep 17 00:00:00 2001 From: nayib-jose-gloria Date: Mon, 18 Sep 2023 10:47:00 -0400 Subject: [PATCH 7/7] lint fix --- cellxgene_schema_cli/cellxgene_schema/validate.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cellxgene_schema_cli/cellxgene_schema/validate.py b/cellxgene_schema_cli/cellxgene_schema/validate.py index b2c21ae57..1e9030c29 100644 --- a/cellxgene_schema_cli/cellxgene_schema/validate.py +++ b/cellxgene_schema_cli/cellxgene_schema/validate.py @@ -1,9 +1,8 @@ import logging import math import os -import re from datetime import datetime -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Union import anndata import numpy as np @@ -761,13 +760,6 @@ def _validate_seurat_convertibility(self): ) self.is_seurat_convertible = False - if self.adata.raw and self.adata.raw.X.shape[1] != self.adata.raw.var.shape[0]: - self.warnings.append( - f"This dataset cannot be converted to the .rds (Seurat v4) format. " - f"There is a mismatch in the number of variables in the raw matrix and the raw var key-indexed " - f"variables." - ) - self.is_seurat_convertible = False if self.adata.raw and self.adata.raw.X.shape[1] != self.adata.raw.var.shape[0]: self.warnings.append(