From 478648ef138d4d6d26ee43bd68c84414fe2292fc Mon Sep 17 00:00:00 2001 From: Evan Molinelli Date: Tue, 26 Nov 2024 11:41:54 -0500 Subject: [PATCH] feat: update validation for uns['spatial'] (#1129) Co-authored-by: Evan Molinelli Co-authored-by: Nayib Gloria <55710092+nayib-jose-gloria@users.noreply.github.com> --- .../cellxgene_schema/validate.py | 42 ++-- .../tests/test_schema_compliance.py | 11 +- cellxgene_schema_cli/tests/test_validate.py | 201 +++++++++++++----- 3 files changed, 179 insertions(+), 75 deletions(-) diff --git a/cellxgene_schema_cli/cellxgene_schema/validate.py b/cellxgene_schema_cli/cellxgene_schema/validate.py index f7892e6b6..024a51ec0 100644 --- a/cellxgene_schema_cli/cellxgene_schema/validate.py +++ b/cellxgene_schema_cli/cellxgene_schema/validate.py @@ -28,8 +28,15 @@ VISIUM_AND_IS_SINGLE_TRUE_MATRIX_SIZE = 4992 SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE = 2000 +SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM = 4000 -ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE = "descendants of obs['assay_ontology_term_id'] 'EFO:0010961' (Visium Spatial Gene Expression) and uns['spatial']['is_single'] is True" +CONDITION_IS_VISIUM = "a descendant of 'EFO:0010961' (Visium Spatial Gene Expression)" +CONDITION_IS_SEQV2 = f"'{ASSAY_SLIDE_SEQV2}' (Slide-seqV2)" + + +ERROR_SUFFIX_SPATIAL = f"obs['assay_ontology_term_id'] is either {CONDITION_IS_VISIUM} or {CONDITION_IS_SEQV2}" +ERROR_SUFFIX_VISIUM = f"obs['assay_ontology_term_id'] is {CONDITION_IS_VISIUM}" +ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE = f"{ERROR_SUFFIX_VISIUM} and uns['spatial']['is_single'] is True" ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE_FORBIDDEN = f"is only allowed for {ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE}" ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE_REQUIRED = f"is required for {ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE}" ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE_IN_TISSUE_0 = f"{ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE} and in_tissue is 0" @@ -95,9 +102,11 @@ def _is_supported_spatial_assay(self) -> bool: """ if self.is_spatial is None: try: - self.is_spatial = False - if self.adata.obs.assay_ontology_term_id.isin([ASSAY_VISIUM, ASSAY_SLIDE_SEQV2]).any(): - self.is_spatial = True + _spatial = ( + self._is_visium_including_descendants() + or self.adata.obs.assay_ontology_term_id.isin([ASSAY_SLIDE_SEQV2]).any() + ) + self.is_spatial = bool(_spatial) except AttributeError: # specific error reporting will occur downstream in the validation self.is_spatial = False @@ -1466,10 +1475,7 @@ def _validate_spatial_assay_ontology_term_id(self): # Validate assay ontology term ids are identical. term_count = obs["assay_ontology_term_id"].nunique() if term_count > 1: - self.errors.append( - "When obs['assay_ontology_term_id'] is either 'EFO:0010961' (Visium Spatial Gene Expression) or " - "'EFO:0030062' (Slide-seqV2), all observations must contain the same value." - ) + self.errors.append(f"When {ERROR_SUFFIX_SPATIAL}" ", all observations must contain the same value.") def _validate_spatial_cell_type_ontology_term_id(self): """ @@ -1599,10 +1605,7 @@ def _check_spatial_uns(self): uns_spatial = self.adata.uns.get("spatial") is_supported_spatial_assay = self._is_supported_spatial_assay() if uns_spatial is not None and not is_supported_spatial_assay: - self.errors.append( - "uns['spatial'] is only allowed for obs['assay_ontology_term_id'] values " - "'EFO:0010961' (Visium Spatial Gene Expression) and 'EFO:0030062' (Slide-seqV2)." - ) + self.errors.append(f"uns['spatial'] is only allowed when {ERROR_SUFFIX_SPATIAL}") return # Exit if we aren't dealing with a supported spatial assay as no further checks are necessary. @@ -1611,10 +1614,7 @@ def _check_spatial_uns(self): # spatial is required for supported spatial assays. if not isinstance(uns_spatial, dict): - self.errors.append( - "A dict in uns['spatial'] is required for obs['assay_ontology_term_id'] values " - "'EFO:0010961' (Visium Spatial Gene Expression) and 'EFO:0030062' (Slide-seqV2)." - ) + self.errors.append("A dict in uns['spatial'] is required when " f"{ERROR_SUFFIX_SPATIAL}.") return # is_single is required. @@ -1693,7 +1693,11 @@ def _check_spatial_uns(self): self.errors.append("uns['spatial'][library_id]['images'] must contain the key 'hires'.") # hires is specified: proceed with validation of hires. else: - self._validate_spatial_image_shape("hires", uns_images["hires"], SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE) + _assay_term = self.adata.obs["assay_ontology_term_id"].values[0] + _max_size = SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE + if is_ontological_descendant_of(ONTOLOGY_PARSER, _assay_term, "EFO:0022860", True): + _max_size = SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM + self._validate_spatial_image_shape("hires", uns_images["hires"], _max_size) # fullres is optional. uns_fullres = uns_images.get("fullres") @@ -1802,12 +1806,12 @@ def _is_visium_including_descendants(self) -> bool: # check if any assay_ontology_term_ids are descendants of VISIUM includes_and_visium = ( self.adata.obs[_assay_key] + .astype("string") .apply(lambda assay: is_ontological_descendant_of(ONTOLOGY_PARSER, assay, ASSAY_VISIUM, True)) .any() ) + self.is_visium = includes_and_visium - # save state and return - self.is_visium = includes_and_visium return includes_and_visium def _validate_spatial_image_shape(self, image_name: str, image: np.ndarray, max_dimension: int = None): diff --git a/cellxgene_schema_cli/tests/test_schema_compliance.py b/cellxgene_schema_cli/tests/test_schema_compliance.py index 7268f332a..425086fcd 100644 --- a/cellxgene_schema_cli/tests/test_schema_compliance.py +++ b/cellxgene_schema_cli/tests/test_schema_compliance.py @@ -4,6 +4,7 @@ import tempfile import unittest +from copy import deepcopy import anndata import fixtures.examples_validate as examples @@ -495,7 +496,7 @@ def test_column_presence_in_tissue(self, validator_with_visium_assay, assay_onto assert validator.errors == [] else: assert validator.errors == [ - "obs['in_tissue'] is only allowed for descendants of obs['assay_ontology_term_id'] 'EFO:0010961' (Visium Spatial Gene Expression) and uns['spatial']['is_single'] is True." + "obs['in_tissue'] is only allowed for obs['assay_ontology_term_id'] is a descendant of 'EFO:0010961' (Visium Spatial Gene Expression) and uns['spatial']['is_single'] is True." ] @pytest.mark.parametrize("reserved_column", schema_def["components"]["obs"]["reserved_columns"]) @@ -1673,11 +1674,16 @@ def test_should_warn_for_low_gene_count(self, validator_with_adata): Raise a warning if there are too few genes """ validator = validator_with_adata + # NOTE:[EM] changing the schema def here is stateful and results in unpredictable test results. + # Reset after mutating. + _old_schema = deepcopy(validator.schema_def.copy()) + validator.schema_def["components"]["var"]["warn_if_less_than_rows"] = 100 validator.validate_adata() assert validator.warnings == [ "WARNING: Dataframe 'var' only has 4 rows. Features SHOULD NOT be filtered from expression matrix." ] + validator.schema_def = _old_schema @pytest.mark.parametrize( "df,column", @@ -2198,7 +2204,6 @@ def test_obsm_values_no_X_embedding__non_spatial_dataset(self, validator_with_ad ] assert validator.is_spatial is False assert validator.warnings == [ - "WARNING: Dataframe 'var' only has 4 rows. Features SHOULD NOT be filtered from expression matrix.", "WARNING: Embedding key in 'adata.obsm' harmony is not 'spatial' nor does it start with 'X_'. " "Thus, it will not be available in Explorer", "WARNING: Validation of raw layer was not performed due to current errors, try again after fixing current errors.", @@ -2248,7 +2253,6 @@ def test_obsm_values_warn_start_with_X(self, validator_with_adata): validator.adata.obsm["harmony"] = pd.DataFrame(validator.adata.obsm["X_umap"], index=validator.adata.obs_names) validator.validate_adata() assert validator.warnings == [ - "WARNING: Dataframe 'var' only has 4 rows. Features SHOULD NOT be filtered from expression matrix.", "WARNING: Embedding key in 'adata.obsm' harmony is not 'spatial' nor does it start with 'X_'. " "Thus, it will not be available in Explorer", "WARNING: Validation of raw layer was not performed due to current errors, try again after fixing current errors.", @@ -2282,7 +2286,6 @@ def test_obsm_values_key_start_with_number(self, validator_with_adata): "'pandas.core.frame.DataFrame'>').", ] assert validator.warnings == [ - "WARNING: Dataframe 'var' only has 4 rows. Features SHOULD NOT be filtered from expression matrix.", "WARNING: Embedding key in 'adata.obsm' 3D is not 'spatial' nor does it start with 'X_'. " "Thus, it will not be available in Explorer", "WARNING: Validation of raw layer was not performed due to current errors, try again after fixing current errors.", diff --git a/cellxgene_schema_cli/tests/test_validate.py b/cellxgene_schema_cli/tests/test_validate.py index accc6b86f..9ab301779 100644 --- a/cellxgene_schema_cli/tests/test_validate.py +++ b/cellxgene_schema_cli/tests/test_validate.py @@ -15,6 +15,8 @@ ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE_FORBIDDEN, ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE_IN_TISSUE_0, ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE_REQUIRED, + SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE, + SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM, Validator, validate, ) @@ -423,10 +425,9 @@ def test__validate_spatial_type_error(self, spatial): # Confirm key type dict is required. validator.validate_adata() - assert validator.errors assert ( - "A dict in uns['spatial'] is required for obs['assay_ontology_term_id'] values 'EFO:0010961' (Visium Spatial Gene Expression) and 'EFO:0030062' (Slide-seqV2)." - in validator.errors[0] + validator.errors[0] + == "ERROR: A dict in uns['spatial'] is required when obs['assay_ontology_term_id'] is either a descendant of 'EFO:0010961' (Visium Spatial Gene Expression) or 'EFO:0030062' (Slide-seqV2)." ) def test__validate_spatial_is_single_false_ok(self): @@ -448,25 +449,42 @@ def test__validate_spatial_forbidden_if_not_visium_or_slide_seqv2(self): # Confirm spatial is not allowed for 10x 3' v2. validator._check_spatial_uns() - assert len(validator.errors) == 1 - assert ( - "uns['spatial'] is only allowed for obs['assay_ontology_term_id'] values " - "'EFO:0010961' (Visium Spatial Gene Expression) and 'EFO:0030062' (Slide-seqV2)." in validator.errors[0] - ) + assert validator.errors == [ + "uns['spatial'] is only allowed when obs['assay_ontology_term_id'] is either " + "a descendant of 'EFO:0010961' (Visium Spatial Gene Expression) or 'EFO:0030062' (Slide-seqV2)" + ] - def test__validate_spatial_required_if_visium(self): + @pytest.mark.parametrize( + "assay_ontology_term_id, is_descendant", + [("EFO:0010961", True), ("EFO:0022858", True), ("EFO:0030029", False), ("EFO:0002697", False)], + ) + def test__validate_spatial_required_if_visium(self, assay_ontology_term_id, is_descendant): validator: Validator = Validator() validator._set_schema_def() validator.adata = adata_visium.copy() - validator.adata.uns = good_uns.copy() + validator.adata.obs["assay_ontology_term_id"] = assay_ontology_term_id - # Confirm spatial is required for Visium. - validator._check_spatial_uns() - assert len(validator.errors) == 1 - assert ( - "A dict in uns['spatial'] is required for obs['assay_ontology_term_id'] values " - "'EFO:0010961' (Visium Spatial Gene Expression) and 'EFO:0030062' (Slide-seqV2)." in validator.errors[0] - ) + if is_descendant: + # check pass if 'spatial' included + validator.adata.uns = good_uns_with_visium_spatial.copy() + validator._check_spatial_uns() + assert len(validator.errors) == 0 + validator.reset() + + # check fail if 'spatial' not included + validator.adata.uns = good_uns.copy() + validator._check_spatial_uns() + assert validator.errors == [ + "A dict in uns['spatial'] is required when obs['assay_ontology_term_id'] is " + "either a descendant of 'EFO:0010961' (Visium Spatial Gene Expression) or 'EFO:0030062' (Slide-seqV2)." + ] + validator.reset() + else: + # check fail if 'spatial' included + validator.adata.uns = good_uns_with_visium_spatial.copy() + validator._check_spatial_uns() + assert len(validator.errors) == 1 + validator.reset() def test__validate_spatial_required_if_slide_seqV2(self): validator: Validator = Validator() @@ -476,11 +494,9 @@ def test__validate_spatial_required_if_slide_seqV2(self): # Confirm spatial is required for Slide-seqV2. validator._check_spatial_uns() - assert len(validator.errors) == 1 - assert ( - "A dict in uns['spatial'] is required for obs['assay_ontology_term_id'] values " - "'EFO:0010961' (Visium Spatial Gene Expression) and 'EFO:0030062' (Slide-seqV2)." in validator.errors[0] - ) + assert validator.errors == [ + "A dict in uns['spatial'] is required when obs['assay_ontology_term_id'] is either a descendant of 'EFO:0010961' (Visium Spatial Gene Expression) or 'EFO:0030062' (Slide-seqV2)." + ] def test__validate_spatial_allowed_keys_error(self): validator: Validator = Validator() @@ -496,16 +512,26 @@ def test__validate_spatial_allowed_keys_error(self): "More than two top-level keys detected:" in validator.errors[0] ) - def test__validate_is_single_required_visium_error(self): + @pytest.mark.parametrize( + "assay_ontology_term_id, is_descendant", + [("EFO:0010961", True), ("EFO:0022858", True), ("EFO:0030029", False), ("EFO:0002697", False)], + ) + def test__validate_is_single_required_visium_error(self, assay_ontology_term_id, is_descendant): validator: Validator = Validator() validator._set_schema_def() validator.adata = adata_visium.copy() + validator.adata.obs["assay_ontology_term_id"] = assay_ontology_term_id validator.adata.uns["spatial"].pop("is_single") - - # Confirm is_single is identified as required. validator._check_spatial_uns() - assert validator.errors - assert "uns['spatial'] must contain the key 'is_single'." in validator.errors[0] + + if is_descendant: + # if spatial, MUST specify `is_single` + assert "uns['spatial'] must contain the key 'is_single'." in validator.errors[0] + else: + # if not spatial, MUST NOT speciffy `is_single` + assert validator.errors == [ + "uns['spatial'] is only allowed when obs['assay_ontology_term_id'] is either a descendant of 'EFO:0010961' (Visium Spatial Gene Expression) or 'EFO:0030062' (Slide-seqV2)" + ] def test__validate_is_single_required_slide_seqV2_error(self): validator: Validator = Validator() @@ -560,19 +586,36 @@ def test__validate_library_id_forbidden_if_visium_or_is_single_false(self): assert len(validator.errors) == 1 assert f"uns['spatial'][library_id] {ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE_FORBIDDEN}." in validator.errors[0] - def test__validate_library_id_required_if_visium(self): + @pytest.mark.parametrize( + "assay_ontology_term_id, is_descendant", + [("EFO:0010961", True), ("EFO:0022858", True), ("EFO:0030029", False), ("EFO:0002697", False)], + ) + def test__validate_library_id_required_if_visium(self, assay_ontology_term_id, is_descendant): validator: Validator = Validator() validator._set_schema_def() validator.adata = adata_visium.copy() - validator.adata.uns["spatial"].pop(visium_library_id) - # Confirm library_id is identified as required. - validator._check_spatial_uns() - assert validator.errors - assert ( - f"uns['spatial'] must contain at least one key representing the library_id when {ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE}." - in validator.errors[0] - ) + validator.adata.obs["assay_ontology_term_id"] = assay_ontology_term_id + if is_descendant: + # if spatial, `library_id` must exist + validator._check_spatial_uns() + assert len(validator.errors) == 0 + validator.reset() + + # if spatial, but missing from `uns` + validator.adata.uns["spatial"].pop(visium_library_id) + validator._check_spatial_uns() + assert validator.errors == [ + f"uns['spatial'] must contain at least one key representing the library_id when {ERROR_SUFFIX_VISIUM_AND_IS_SINGLE_TRUE}." + ] + else: + # if not spatial, MUST NOT define `library_id` + validator.adata.uns["spatial"][visium_library_id] = {"images": []} + validator._check_spatial_uns() + # Report the most general top level error + assert validator.errors == [ + "uns['spatial'] is only allowed when obs['assay_ontology_term_id'] is either a descendant of 'EFO:0010961' (Visium Spatial Gene Expression) or 'EFO:0030062' (Slide-seqV2)" + ] @pytest.mark.parametrize("library_id", [None, "invalid", 1, 1.0, True]) def test__validate_library_id_type_error(self, library_id): @@ -610,7 +653,11 @@ def test__validate_images_required_error(self): assert validator.errors assert "uns['spatial'][library_id] must contain the key 'images'." in validator.errors[0] - def test__validate_images_allowed_keys_error(self): + @pytest.mark.parametrize( + "assay_ontology_term_id, is_descendant", + [("EFO:0010961", True), ("EFO:0022858", True), ("EFO:0030029", False), ("EFO:0002697", False)], + ) + def test__validate_images_allowed_keys_error(self, assay_ontology_term_id, is_descendant): validator: Validator = Validator() validator._set_schema_def() validator.adata = adata_visium.copy() @@ -730,34 +777,84 @@ def test__validate_images_image_is_shape_error(self, image_name): "for example) or 4 (RGBA color model for example) for its last dimension" in validator.errors[0] ) - def test__validate_images_hires_max_dimension_greater_than_error(self): + @pytest.mark.parametrize( + "assay_ontology_term_id, hi_res_size, image_max", + [ + ("EFO:0022858", 2001, SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE), + ("EFO:0022860", 4001, SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM), + ], + ) + def test__validate_images_hires_max_dimension_greater_than_error( + self, assay_ontology_term_id, hi_res_size, image_max + ): validator: Validator = Validator() validator._set_schema_def() validator.adata = adata_visium.copy() - validator.adata.uns["spatial"][visium_library_id]["images"]["hires"] = np.zeros((1, 2001, 3), dtype=np.uint8) + validator.adata.obs["assay_ontology_term_id"] = assay_ontology_term_id + validator.adata.uns["spatial"][visium_library_id]["images"]["hires"] = np.zeros( + (1, hi_res_size, 3), dtype=np.uint8 + ) # Confirm hires is identified as invalid. validator._check_spatial_uns() - assert validator.errors - assert ( - "The largest dimension of uns['spatial'][library_id]['images']['hires'] must be 2000 pixels" - in validator.errors[0] - ) + assert validator.errors == [ + f"The largest dimension of uns['spatial'][library_id]['images']['hires'] must be {image_max} pixels, it has a largest dimension of {hi_res_size} pixels." + ] - def test__validate_images_hires_max_dimension_less_than_error(self): + @pytest.mark.parametrize( + "assay_ontology_term_id, hi_res_size, size_requirement", + [ + ("EFO:0022858", SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE, SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE), + ("EFO:0022858", SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM, SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE), + ("EFO:0022860", SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE, SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM), + ( + "EFO:0022860", + SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM, + SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM, + ), + ], + ) + def test__validate_images_hires_max_dimension(self, assay_ontology_term_id, hi_res_size, size_requirement): validator: Validator = Validator() validator._set_schema_def() validator.adata = adata_visium.copy() - validator.adata.uns["spatial"][visium_library_id]["images"]["hires"] = np.zeros((1, 1999, 3), dtype=np.uint8) + validator.adata.obs["assay_ontology_term_id"] = assay_ontology_term_id + validator.adata.uns["spatial"][visium_library_id]["images"]["hires"] = np.zeros( + (1, hi_res_size, 3), dtype=np.uint8 + ) # Confirm hires is identified as invalid. + validator.reset() validator._check_spatial_uns() - assert validator.errors - assert ( - "The largest dimension of uns['spatial'][library_id]['images']['hires'] must be 2000 pixels" - in validator.errors[0] + if hi_res_size == size_requirement: + assert validator.errors == [] + else: + assert validator.errors == [ + f"The largest dimension of uns['spatial'][library_id]['images']['hires'] must be {size_requirement} pixels, it has a largest dimension of {hi_res_size} pixels." + ] + + @pytest.mark.parametrize( + "assay_ontology_term_id, hi_res_size, image_max", + [ + ("EFO:0022858", 1999, SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE), + ("EFO:0022860", 3999, SPATIAL_HIRES_IMAGE_MAX_DIMENSION_SIZE_VISIUM_11MM), + ], + ) + def test__validate_images_hires_max_dimension_less_than_error(self, assay_ontology_term_id, hi_res_size, image_max): + validator: Validator = Validator() + validator._set_schema_def() + validator.adata = adata_visium.copy() + validator.adata.obs["assay_ontology_term_id"] = assay_ontology_term_id + validator.adata.uns["spatial"][visium_library_id]["images"]["hires"] = np.zeros( + (1, hi_res_size, 3), dtype=np.uint8 ) + # Confirm hires is identified as invalid. + validator._check_spatial_uns() + assert validator.errors == [ + f"The largest dimension of uns['spatial'][library_id]['images']['hires'] must be {image_max} pixels, it has a largest dimension of {hi_res_size} pixels." + ] + def test__validate_scalefactors_required_error(self): validator: Validator = Validator() validator._set_schema_def() @@ -861,8 +958,8 @@ def test__validate_assay_type_ontology_term_id_not_unique_error(self): validator._validate_spatial_assay_ontology_term_id() assert validator.errors assert ( - "When obs['assay_ontology_term_id'] is either 'EFO:0010961' (Visium Spatial Gene Expression) or " - "'EFO:0030062' (Slide-seqV2), all observations must contain the same value." + "When obs['assay_ontology_term_id'] is either a descendant" + " of 'EFO:0010961' (Visium Spatial Gene Expression) or 'EFO:0030062' (Slide-seqV2), all observations must contain the same value." ) in validator.errors[0] def test__validate_assay_type_ontology_term_id_not_unique_ok(self, valid_adata):