From 164897467db2a3598c850dabcd42b420072f83f0 Mon Sep 17 00:00:00 2001 From: Kenn Cartier Date: Thu, 29 Aug 2024 15:05:13 -0700 Subject: [PATCH] Renamed class property in compliance with stac standard. Implemented test_layer_parameters in a manner intended to improve ease of code maintenance. --- city_metrix/layers/albedo.py | 16 +- city_metrix/layers/alos_dsm.py | 11 +- .../layers/average_net_building_height.py | 11 +- city_metrix/layers/built_up_height.py | 11 +- city_metrix/layers/esa_world_cover.py | 15 +- .../layers/land_surface_temperature.py | 13 +- city_metrix/layers/nasa_dem.py | 11 +- city_metrix/layers/natural_areas.py | 11 +- city_metrix/layers/ndvi_sentinel2_gee.py | 10 +- city_metrix/layers/tree_canopy_height.py | 11 +- city_metrix/layers/tree_cover.py | 12 +- city_metrix/layers/urban_land_use.py | 12 +- city_metrix/layers/world_pop.py | 11 +- tests/test_layer_parameters.py | 157 ++++++++++-------- 14 files changed, 197 insertions(+), 115 deletions(-) diff --git a/city_metrix/layers/albedo.py b/city_metrix/layers/albedo.py index 92972301..341786b2 100644 --- a/city_metrix/layers/albedo.py +++ b/city_metrix/layers/albedo.py @@ -5,11 +5,19 @@ from .layer import Layer, get_utm_zone_epsg, get_image_collection class Albedo(Layer): - def __init__(self, start_date="2021-01-01", end_date="2022-01-01", scale_meters=10, threshold=None, **kwargs): + """ + Attributes: + start_date: starting date for data retrieval + end_date: ending date for data retrieval + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + threshold: threshold value for filtering the retrieval + """ + + def __init__(self, start_date="2021-01-01", end_date="2022-01-01", spatial_resolution=10, threshold=None, **kwargs): super().__init__(**kwargs) self.start_date = start_date self.end_date = end_date - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution self.threshold = threshold def get_data(self, bbox): @@ -30,7 +38,7 @@ def mask_and_count_clouds(s2wc, geom): nb_cloudy_pixels = is_cloud.reduceRegion( reducer=ee.Reducer.sum().unweighted(), geometry=geom, - scale=self.scale_meters, + scale=self.spatial_resolution, maxPixels=1e9 ) return s2wc.updateMask(is_cloud.eq(0)).set('nb_cloudy_pixels', @@ -89,7 +97,7 @@ def calc_s2_albedo(image): albedo_mean = s2_albedo.reduce(ee.Reducer.mean()) data = (get_image_collection( - ee.ImageCollection(albedo_mean), bbox, self.scale_meters, "albedo") + ee.ImageCollection(albedo_mean), bbox, self.spatial_resolution, "albedo") .albedo_mean) if self.threshold is not None: diff --git a/city_metrix/layers/alos_dsm.py b/city_metrix/layers/alos_dsm.py index 024a9f8d..70000eb5 100644 --- a/city_metrix/layers/alos_dsm.py +++ b/city_metrix/layers/alos_dsm.py @@ -6,9 +6,14 @@ class AlosDSM(Layer): - def __init__(self, scale_meters=30, **kwargs): + """ + Attributes: + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + + def __init__(self, spatial_resolution=30, **kwargs): super().__init__(**kwargs) - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): dataset = ee.ImageCollection("JAXA/ALOS/AW3D30/V3_2") @@ -17,6 +22,6 @@ def get_data(self, bbox): .select('DSM') .mean() ) - data = get_image_collection(alos_dsm, bbox, self.scale_meters, "ALOS DSM").DSM + data = get_image_collection(alos_dsm, bbox, self.spatial_resolution, "ALOS DSM").DSM return data diff --git a/city_metrix/layers/average_net_building_height.py b/city_metrix/layers/average_net_building_height.py index 2a215c77..d0f49f28 100644 --- a/city_metrix/layers/average_net_building_height.py +++ b/city_metrix/layers/average_net_building_height.py @@ -6,9 +6,14 @@ from .layer import Layer, get_utm_zone_epsg, get_image_collection class AverageNetBuildingHeight(Layer): - def __init__(self, scale_meters=100, **kwargs): + """ + Attributes: + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + + def __init__(self, spatial_resolution=100, **kwargs): super().__init__(**kwargs) - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): # https://ghsl.jrc.ec.europa.eu/ghs_buH2023.php @@ -19,7 +24,7 @@ def get_data(self, bbox): anbh = ee.Image("projects/wri-datalab/GHSL/GHS-BUILT-H-ANBH_GLOBE_R2023A") data = (get_image_collection( - ee.ImageCollection(anbh), bbox, self.scale_meters, "average net building height") + ee.ImageCollection(anbh), bbox, self.spatial_resolution, "average net building height") .b1) return data diff --git a/city_metrix/layers/built_up_height.py b/city_metrix/layers/built_up_height.py index bc89d703..ab080f51 100644 --- a/city_metrix/layers/built_up_height.py +++ b/city_metrix/layers/built_up_height.py @@ -7,9 +7,14 @@ class BuiltUpHeight(Layer): - def __init__(self, scale_meters=100, **kwargs): + """ + Attributes: + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + + def __init__(self, spatial_resolution=100, **kwargs): super().__init__(**kwargs) - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): # Notes for Heat project: @@ -19,6 +24,6 @@ def get_data(self, bbox): # ee.ImageCollection("projects/wri-datalab/GHSL/GHS-BUILT-H-ANBH_R2023A") built_height = ee.Image("JRC/GHSL/P2023A/GHS_BUILT_H/2018") - data = get_image_collection(ee.ImageCollection(built_height), bbox, self.scale_meters, "built up height") + data = get_image_collection(ee.ImageCollection(built_height), bbox, self.spatial_resolution, "built up height") return data.built_height diff --git a/city_metrix/layers/esa_world_cover.py b/city_metrix/layers/esa_world_cover.py index b306d8c6..a4b0f65c 100644 --- a/city_metrix/layers/esa_world_cover.py +++ b/city_metrix/layers/esa_world_cover.py @@ -20,29 +20,36 @@ class EsaWorldCoverClass(Enum): MOSS_AND_LICHEN = 100 class EsaWorldCover(Layer): + """ + Attributes: + land_cover_class: Enum value from EsaWorldCoverClass + year: year used for data retrieval + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + STAC_CATALOG_URI = "https://services.terrascope.be/stac/" STAC_COLLECTION_ID = "urn:eop:VITO:ESA_WorldCover_10m_2020_AWS_V1" STAC_ASSET_ID = "ESA_WORLDCOVER_10M_MAP" - def __init__(self, land_cover_class=None, year=2020, scale_meters=10, **kwargs): + def __init__(self, land_cover_class=None, year=2020, spatial_resolution=10, **kwargs): super().__init__(**kwargs) self.land_cover_class = land_cover_class self.year = year - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): if self.year == 2020: data = get_image_collection( ee.ImageCollection("ESA/WorldCover/v100"), bbox, - self.scale_meters, + self.spatial_resolution, "ESA world cover" ).Map elif self.year == 2021: data = get_image_collection( ee.ImageCollection("ESA/WorldCover/v200"), bbox, - self.scale_meters, + self.spatial_resolution, "ESA world cover" ).Map diff --git a/city_metrix/layers/land_surface_temperature.py b/city_metrix/layers/land_surface_temperature.py index da4922cf..931cb2e0 100644 --- a/city_metrix/layers/land_surface_temperature.py +++ b/city_metrix/layers/land_surface_temperature.py @@ -6,11 +6,18 @@ import xarray class LandSurfaceTemperature(Layer): - def __init__(self, start_date="2013-01-01", end_date="2023-01-01", scale_meters=30, **kwargs): + """ + Attributes: + start_date: starting date for data retrieval + end_date: ending date for data retrieval + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + + def __init__(self, start_date="2013-01-01", end_date="2023-01-01", spatial_resolution=30, **kwargs): super().__init__(**kwargs) self.start_date = start_date self.end_date = end_date - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): def cloud_mask(image): @@ -31,5 +38,5 @@ def apply_scale_factors(image): .map(apply_scale_factors) \ .reduce(ee.Reducer.mean()) - data = get_image_collection(ee.ImageCollection(l8_st), bbox, self.scale_meters, "LST").ST_B10_mean + data = get_image_collection(ee.ImageCollection(l8_st), bbox, self.spatial_resolution, "LST").ST_B10_mean return data diff --git a/city_metrix/layers/nasa_dem.py b/city_metrix/layers/nasa_dem.py index 35daa0c5..b5ac45d8 100644 --- a/city_metrix/layers/nasa_dem.py +++ b/city_metrix/layers/nasa_dem.py @@ -6,9 +6,14 @@ class NasaDEM(Layer): - def __init__(self, scale_meters=30, **kwargs): + """ + Attributes: + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + + def __init__(self, spatial_resolution=30, **kwargs): super().__init__(**kwargs) - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): dataset = ee.Image("NASA/NASADEM_HGT/001") @@ -17,6 +22,6 @@ def get_data(self, bbox): .select('elevation') .mean() ) - data = get_image_collection(nasa_dem, bbox, self.scale_meters, "NASA DEM").elevation + data = get_image_collection(nasa_dem, bbox, self.spatial_resolution, "NASA DEM").elevation return data diff --git a/city_metrix/layers/natural_areas.py b/city_metrix/layers/natural_areas.py index 61c648a6..5efe4a8e 100644 --- a/city_metrix/layers/natural_areas.py +++ b/city_metrix/layers/natural_areas.py @@ -5,12 +5,17 @@ from .esa_world_cover import EsaWorldCover, EsaWorldCoverClass class NaturalAreas(Layer): - def __init__(self, scale_meters=10, **kwargs): + """ + Attributes: + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + + def __init__(self, spatial_resolution=10, **kwargs): super().__init__(**kwargs) - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): - esa_world_cover = EsaWorldCover(scale_meters=self.scale_meters).get_data(bbox) + esa_world_cover = EsaWorldCover(spatial_resolution=self.spatial_resolution).get_data(bbox) reclass_map = { EsaWorldCoverClass.TREE_COVER.value: 1, EsaWorldCoverClass.SHRUBLAND.value: 1, diff --git a/city_metrix/layers/ndvi_sentinel2_gee.py b/city_metrix/layers/ndvi_sentinel2_gee.py index ca7fc05e..89b4c09e 100644 --- a/city_metrix/layers/ndvi_sentinel2_gee.py +++ b/city_metrix/layers/ndvi_sentinel2_gee.py @@ -4,16 +4,18 @@ class NdviSentinel2(Layer): """" NDVI = Sentinel-2 Normalized Difference Vegetation Index - param: year: The satellite imaging year. + Attributes: + year: The satellite imaging year. + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) return: a rioxarray-format DataArray Author of associated Jupyter notebook: Ted.Wong@wri.org Notebook: https://github.com/wri/cities-cities4forests-indicators/blob/dev-eric/scripts/extract-VegetationCover.ipynb Reference: https://en.wikipedia.org/wiki/Normalized_difference_vegetation_index """ - def __init__(self, year=None, scale_meters=10, **kwargs): + def __init__(self, year=None, spatial_resolution=10, **kwargs): super().__init__(**kwargs) self.year = year - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): if self.year is None: @@ -40,7 +42,7 @@ def calculate_ndvi(image): ndvi_mosaic = ndvi.qualityMosaic('NDVI') ic = ee.ImageCollection(ndvi_mosaic) - ndvi_data = get_image_collection(ic, bbox, self.scale_meters, "NDVI") + ndvi_data = get_image_collection(ic, bbox, self.spatial_resolution, "NDVI") xdata = ndvi_data.to_dataarray() diff --git a/city_metrix/layers/tree_canopy_height.py b/city_metrix/layers/tree_canopy_height.py index 9141f84e..fee1697b 100644 --- a/city_metrix/layers/tree_canopy_height.py +++ b/city_metrix/layers/tree_canopy_height.py @@ -6,12 +6,17 @@ import ee class TreeCanopyHeight(Layer): + """ + Attributes: + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + name = "tree_canopy_height" NO_DATA_VALUE = 0 - def __init__(self, scale_meters=1, **kwargs): + def __init__(self, spatial_resolution=1, **kwargs): super().__init__(**kwargs) - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): canopy_ht = ee.ImageCollection("projects/meta-forest-monitoring-okw37/assets/CanopyHeight") @@ -19,6 +24,6 @@ def get_data(self, bbox): canopy_ht = canopy_ht.reduce(ee.Reducer.mean()).rename("cover_code") data = get_image_collection(ee.ImageCollection(canopy_ht), bbox, - self.scale_meters, "tree canopy height") + self.spatial_resolution, "tree canopy height") return data.cover_code diff --git a/city_metrix/layers/tree_cover.py b/city_metrix/layers/tree_cover.py index 9caa4830..98bc481d 100644 --- a/city_metrix/layers/tree_cover.py +++ b/city_metrix/layers/tree_cover.py @@ -8,15 +8,19 @@ class TreeCover(Layer): """ Merged tropical and nontropical tree cover from WRI + Attributes: + min_tree_cover: minimum tree-cover values used for filtering results + max_tree_cover: maximum tree-cover values used for filtering results + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) """ - + NO_DATA_VALUE = 255 - def __init__(self, min_tree_cover=None, max_tree_cover=None, scale_meters=10, **kwargs): + def __init__(self, min_tree_cover=None, max_tree_cover=None, spatial_resolution=10, **kwargs): super().__init__(**kwargs) self.min_tree_cover = min_tree_cover self.max_tree_cover = max_tree_cover - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): tropics = ee.ImageCollection('projects/wri-datalab/TropicalTreeCover') @@ -24,7 +28,7 @@ def get_data(self, bbox): merged_ttc = tropics.merge(nontropics) ttc_image = merged_ttc.reduce(ee.Reducer.mean()).rename('ttc') - data = get_image_collection(ee.ImageCollection(ttc_image), bbox, self.scale_meters, "tree cover").ttc + data = get_image_collection(ee.ImageCollection(ttc_image), bbox, self.spatial_resolution, "tree cover").ttc if self.min_tree_cover is not None: data = data.where(data >= self.min_tree_cover) diff --git a/city_metrix/layers/urban_land_use.py b/city_metrix/layers/urban_land_use.py index da832196..fe69c758 100644 --- a/city_metrix/layers/urban_land_use.py +++ b/city_metrix/layers/urban_land_use.py @@ -7,10 +7,16 @@ class UrbanLandUse(Layer): - def __init__(self, band='lulc', scale_meters=5, **kwargs): + """ + Attributes: + band: raster band used for data retrieval + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + + def __init__(self, band='lulc', spatial_resolution=5, **kwargs): super().__init__(**kwargs) self.band = band - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): dataset = ee.ImageCollection("projects/wri-datalab/cities/urban_land_use/V1") @@ -28,6 +34,6 @@ def get_data(self, bbox): .rename('lulc') ) - data = get_image_collection(ulu, bbox, self.scale_meters, "urban land use").lulc + data = get_image_collection(ulu, bbox, self.spatial_resolution, "urban land use").lulc return data diff --git a/city_metrix/layers/world_pop.py b/city_metrix/layers/world_pop.py index d77cc2ed..dff494ab 100644 --- a/city_metrix/layers/world_pop.py +++ b/city_metrix/layers/world_pop.py @@ -6,9 +6,14 @@ from .layer import Layer, get_utm_zone_epsg, get_image_collection class WorldPop(Layer): - def __init__(self, scale_meters=100, **kwargs): + """ + Attributes: + spatial_resolution: raster resolution in meters (see https://github.com/stac-extensions/raster) + """ + + def __init__(self, spatial_resolution=100, **kwargs): super().__init__(**kwargs) - self.scale_meters = scale_meters + self.spatial_resolution = spatial_resolution def get_data(self, bbox): # load population @@ -20,5 +25,5 @@ def get_data(self, bbox): .mean() ) - data = get_image_collection(world_pop, bbox, self.scale_meters, "world pop") + data = get_image_collection(world_pop, bbox, self.spatial_resolution, "world pop") return data.population diff --git a/tests/test_layer_parameters.py b/tests/test_layer_parameters.py index bef67ec6..ce220a5a 100644 --- a/tests/test_layer_parameters.py +++ b/tests/test_layer_parameters.py @@ -1,93 +1,106 @@ from city_metrix.layers import ( + Layer, Albedo, AlosDSM, AverageNetBuildingHeight, + BuiltUpHeight, EsaWorldCover, EsaWorldCoverClass, LandSurfaceTemperature, NasaDEM, NaturalAreas, + NdviSentinel2, TreeCanopyHeight, TreeCover, UrbanLandUse, - WorldPop, NdviSentinel2, BuiltUpHeight + WorldPop ) from tests.resources.bbox_constants import BBOX_BRA_LAURO_DE_FREITAS_1 +""" +Note: To add a test for another scalable layer that has the spatial_resolution property: +1. Add the class name to the city_metrix.layers import statement above +2. Specify a minimal class instance in the set below. Do no specify the spatial_resolution + property in the instance definition. +""" +CLASSES_WITH_spatial_resolution_PROPERTY = \ + { + 'Albedo()', + 'AlosDSM()', + 'AverageNetBuildingHeight()', + 'BuiltUpHeight()', + 'EsaWorldCover(land_cover_class=EsaWorldCoverClass.BUILT_UP)', + 'LandSurfaceTemperature()', + 'NasaDEM()', + 'NaturalAreas()', + 'NdviSentinel2(year=2023)', + 'TreeCanopyHeight()', + 'TreeCover()', + 'UrbanLandUse()', + 'WorldPop()' + } + COUNTRY_CODE_FOR_BBOX = 'BRA' BBOX = BBOX_BRA_LAURO_DE_FREITAS_1 -def test_albedo_scale(): - doubled_default_resolution = 2 * Albedo().scale_meters - layer = Albedo(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_alos_dsm_scale(): - doubled_default_resolution = 2 * AlosDSM().scale_meters - layer = AlosDSM(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_average_net_building_height_scale(): - doubled_default_resolution = 2 * AverageNetBuildingHeight().scale_meters - layer = AverageNetBuildingHeight(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_built_up_height_scale(): - doubled_default_resolution = 2 * BuiltUpHeight().scale_meters - layer = BuiltUpHeight(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_esa_world_cover_scale(): - doubled_default_resolution = 2 * EsaWorldCover().scale_meters - layer = EsaWorldCover(land_cover_class=EsaWorldCoverClass.BUILT_UP, scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -# TODO -# def test_high_land_surface_temperature_scale(): - -def test_land_surface_temperature_scale(): - doubled_default_resolution = 2 * LandSurfaceTemperature().scale_meters - layer = LandSurfaceTemperature(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_nasa_dem_scale(): - doubled_default_resolution = 2 * NasaDEM().scale_meters - layer = NasaDEM(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_natural_areas_scale(): - doubled_default_resolution = 2 * NaturalAreas().scale_meters - layer = NaturalAreas(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_ndvi_sentinel2_scale(): - doubled_default_resolution = 2 * NdviSentinel2().scale_meters - layer = NdviSentinel2(year=2023, scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -# TODO -# def test_smart_surface_lulc_scale(): - -def test_tree_canopy_height(): - doubled_default_resolution = 2 * TreeCanopyHeight().scale_meters - layer = TreeCanopyHeight(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_tree_cover(): - doubled_default_resolution = 2 * TreeCover().scale_meters - layer = TreeCover(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_urban_land_use(): - doubled_default_resolution = 2 * UrbanLandUse().scale_meters - layer = UrbanLandUse(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - -def test_world_pop(): - doubled_default_resolution = 2 * WorldPop().scale_meters - layer = WorldPop(scale_meters=doubled_default_resolution) - evaluate_layer(layer, doubled_default_resolution) - +def test_scale__meters_property_for_all_scalable_layers(): + for class_instance_str in CLASSES_WITH_spatial_resolution_PROPERTY: + is_valid, except_str = validate_layer_instance(class_instance_str) + if is_valid is False: + raise Exception(except_str) + + class_instance = eval(class_instance_str) + + # Double the spatial_resolution for the specified Class + doubled_default_resolution = 2 * class_instance.spatial_resolution + class_instance.spatial_resolution=doubled_default_resolution + + evaluate_layer(class_instance, doubled_default_resolution) + + +def test_function_validate_layer_instance(): + is_valid, except_str = validate_layer_instance(Albedo()) + assert is_valid is False + is_valid, except_str = validate_layer_instance('t') + assert is_valid is False + is_valid, except_str = validate_layer_instance('Layer()') + assert is_valid is False + is_valid, except_str = validate_layer_instance('OpenStreetMap()') + assert is_valid is False + is_valid, except_str = validate_layer_instance('Albedo(spatial_resolution = 2)') + assert is_valid is False + +def validate_layer_instance(obj_string): + is_valid = True + except_str = None + obj_eval = None + + if not type(obj_string) == str: + is_valid = False + except_str = "Specified object '%s' must be specified as a string." % obj_string + return is_valid, except_str + + try: + obj_eval = eval(obj_string) + except: + is_valid = False + except_str = "Specified object '%s' is not a class instance." % obj_string + return is_valid, except_str + + if not type(obj_eval).__bases__[0] == Layer: + is_valid = False + except_str = "Specified object '%s' is not a valid Layer class instance." % obj_string + elif not hasattr(obj_eval, 'spatial_resolution'): + is_valid = False + except_str = "Specified class '%s' does not have the spatial_resolution property." % obj_string + elif not obj_string.find('spatial_resolution') == -1: + is_valid = False + except_str = "Do not specify spatial_resolution property value in object '%s'." % obj_string + elif obj_eval.spatial_resolution is None: + is_valid = False + except_str = "Class signature cannot specify None for default value for class." + + return is_valid, except_str def evaluate_layer(layer, expected_resolution): data = layer.get_data(BBOX)