From 92eb7919ede80b74d2d64bed6d3fbd1d0064a1ca Mon Sep 17 00:00:00 2001 From: Kenn Cartier Date: Fri, 13 Dec 2024 11:58:34 -0800 Subject: [PATCH 1/3] layers cleanup --- city_metrix/layers/albedo.py | 69 +++++++---- city_metrix/layers/alos_dsm.py | 23 ++-- .../layers/average_net_building_height.py | 19 +-- city_metrix/layers/built_up_height.py | 18 +-- city_metrix/layers/esa_world_cover.py | 29 ++--- city_metrix/layers/glad_lulc.py | 14 ++- .../layers/high_land_surface_temperature.py | 41 ++++--- city_metrix/layers/impervious_surface.py | 31 +++-- .../layers/land_surface_temperature.py | 35 +++--- city_metrix/layers/layer.py | 1 - city_metrix/layers/nasa_dem.py | 24 ++-- city_metrix/layers/natural_areas.py | 7 +- city_metrix/layers/ndvi_sentinel2_gee.py | 11 +- city_metrix/layers/smart_surface_lulc.py | 43 ++++--- city_metrix/layers/tree_canopy_height.py | 24 ++-- city_metrix/layers/tree_cover.py | 28 +++-- city_metrix/layers/urban_land_use.py | 22 ++-- city_metrix/layers/world_pop.py | 54 ++++++--- .../conftest.py | 11 +- .../layers_for_br_lauro_de_freitas.qgz | Bin 25729 -> 0 bytes ...qgis_files.py => test_write_all_layers.py} | 73 +++--------- .../test_write_layers_other.py | 31 +++++ ...est_write_layers_using_fixed_resolution.py | 111 ++++++++++++++++++ 23 files changed, 471 insertions(+), 248 deletions(-) delete mode 100644 tests/resources/layer_dumps_for_br_lauro_de_freitas/layers_for_br_lauro_de_freitas.qgz rename tests/resources/layer_dumps_for_br_lauro_de_freitas/{test_write_layers_to_qgis_files.py => test_write_all_layers.py} (81%) create mode 100644 tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_other.py create mode 100644 tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_using_fixed_resolution.py diff --git a/city_metrix/layers/albedo.py b/city_metrix/layers/albedo.py index 341786b2..dc1780a1 100644 --- a/city_metrix/layers/albedo.py +++ b/city_metrix/layers/albedo.py @@ -1,8 +1,6 @@ import ee -import xarray -from dask.diagnostics import ProgressBar -from .layer import Layer, get_utm_zone_epsg, get_image_collection +from .layer import Layer, get_image_collection class Albedo(Layer): """ @@ -34,19 +32,33 @@ def get_data(self, bbox): def mask_and_count_clouds(s2wc, geom): s2wc = ee.Image(s2wc) geom = ee.Geometry(geom.geometry()) - is_cloud = ee.Image(s2wc.get('cloud_mask')).gt(MAX_CLOUD_PROB).rename('is_cloud') + is_cloud = (ee.Image(s2wc.get('cloud_mask')) + .gt(MAX_CLOUD_PROB) + .rename('is_cloud') + ) + nb_cloudy_pixels = is_cloud.reduceRegion( reducer=ee.Reducer.sum().unweighted(), geometry=geom, scale=self.spatial_resolution, maxPixels=1e9 ) - return s2wc.updateMask(is_cloud.eq(0)).set('nb_cloudy_pixels', - nb_cloudy_pixels.getNumber('is_cloud')).divide(10000) + mask = (s2wc + .updateMask(is_cloud.eq(0)) + .set('nb_cloudy_pixels',nb_cloudy_pixels.getNumber('is_cloud')) + .divide(10000) + ) + + return mask def mask_clouds_and_rescale(im): - clouds = ee.Image(im.get('cloud_mask')).select('probability') - return im.updateMask(clouds.lt(MAX_CLOUD_PROB)).divide(10000) + clouds = ee.Image(im.get('cloud_mask') + ).select('probability') + mask = im.updateMask(clouds + .lt(MAX_CLOUD_PROB) + ).divide(10000) + + return mask def get_masked_s2_collection(roi, start, end): criteria = (ee.Filter.And( @@ -55,23 +67,29 @@ def get_masked_s2_collection(roi, start, end): )) s2 = S2.filter(criteria) # .select('B2','B3','B4','B8','B11','B12') s2c = S2C.filter(criteria) - s2_with_clouds = (ee.Join.saveFirst('cloud_mask').apply(**{ - 'primary': ee.ImageCollection(s2), - 'secondary': ee.ImageCollection(s2c), - 'condition': ee.Filter.equals(**{'leftField': 'system:index', 'rightField': 'system:index'}) - })) + s2_with_clouds = ( + ee.Join.saveFirst('cloud_mask') + .apply(**{ + 'primary': ee.ImageCollection(s2), + 'secondary': ee.ImageCollection(s2c), + 'condition': ee.Filter.equals(**{'leftField': 'system:index', 'rightField': 'system:index'}) + }) + ) def _mcc(im): return mask_and_count_clouds(im, roi) # s2_with_clouds=ee.ImageCollection(s2_with_clouds).map(_mcc) # s2_with_clouds=s2_with_clouds.limit(image_limit,'nb_cloudy_pixels') - s2_with_clouds = ee.ImageCollection(s2_with_clouds).map( - mask_clouds_and_rescale) # .limit(image_limit,'CLOUDY_PIXEL_PERCENTAGE') - return ee.ImageCollection(s2_with_clouds) + s2_with_clouds = (ee.ImageCollection(s2_with_clouds) + .map(mask_clouds_and_rescale) + ) # .limit(image_limit,'CLOUDY_PIXEL_PERCENTAGE') - # calculate albedo for images + s2_with_clouds_ic = ee.ImageCollection(s2_with_clouds) + + return s2_with_clouds_ic + # calculate albedo for images # weights derived from # S. Bonafoni and A. Sekertekin, "Albedo Retrieval From Sentinel-2 by New Narrow-to-Broadband Conversion Coefficients," in IEEE Geoscience and Remote Sensing Letters, vol. 17, no. 9, pp. 1618-1622, Sept. 2020, doi: 10.1109/LGRS.2020.2967085. def calc_s2_albedo(image): @@ -89,20 +107,25 @@ def calc_s2_albedo(image): 'SWIR1': image.select('B11'), 'SWIR2': image.select('B12') } - return image.expression(S2_ALBEDO_EQN, config).double().rename('albedo') + + albedo = image.expression(S2_ALBEDO_EQN, config).double().rename('albedo') + + return albedo ## S2 MOSAIC AND ALBEDO dataset = get_masked_s2_collection(ee.Geometry.BBox(*bbox), self.start_date, self.end_date) s2_albedo = dataset.map(calc_s2_albedo) albedo_mean = s2_albedo.reduce(ee.Reducer.mean()) - data = (get_image_collection( - ee.ImageCollection(albedo_mean), bbox, self.spatial_resolution, "albedo") - .albedo_mean) + albedo_mean_ic = ee.ImageCollection(albedo_mean) + data = get_image_collection( + albedo_mean_ic, + bbox, + self.spatial_resolution, + "albedo" + ).albedo_mean if self.threshold is not None: return data.where(data < self.threshold) return data - - diff --git a/city_metrix/layers/alos_dsm.py b/city_metrix/layers/alos_dsm.py index 70000eb5..c22df829 100644 --- a/city_metrix/layers/alos_dsm.py +++ b/city_metrix/layers/alos_dsm.py @@ -1,6 +1,4 @@ import ee -import xee -import xarray as xr from .layer import Layer, get_image_collection @@ -16,12 +14,19 @@ def __init__(self, spatial_resolution=30, **kwargs): self.spatial_resolution = spatial_resolution def get_data(self, bbox): - dataset = ee.ImageCollection("JAXA/ALOS/AW3D30/V3_2") - alos_dsm = ee.ImageCollection(dataset - .filterBounds(ee.Geometry.BBox(*bbox)) - .select('DSM') - .mean() - ) - data = get_image_collection(alos_dsm, bbox, self.spatial_resolution, "ALOS DSM").DSM + alos_dsm = ee.ImageCollection("JAXA/ALOS/AW3D30/V3_2") + + alos_dsm_ic = ee.ImageCollection(alos_dsm + .filterBounds(ee.Geometry.BBox(*bbox)) + .select('DSM') + .mean() + ) + + data = get_image_collection( + alos_dsm_ic, + 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 d0f49f28..11799cce 100644 --- a/city_metrix/layers/average_net_building_height.py +++ b/city_metrix/layers/average_net_building_height.py @@ -1,9 +1,7 @@ -from dask.diagnostics import ProgressBar -import xarray as xr -import xee import ee -from .layer import Layer, get_utm_zone_epsg, get_image_collection +from .layer import Layer, get_image_collection + class AverageNetBuildingHeight(Layer): """ @@ -23,8 +21,13 @@ def get_data(self, bbox): # GLOBE - ee.Image("projects/wri-datalab/GHSL/GHS-BUILT-H-ANBH_GLOBE_R2023A") anbh = ee.Image("projects/wri-datalab/GHSL/GHS-BUILT-H-ANBH_GLOBE_R2023A") - data = (get_image_collection( - ee.ImageCollection(anbh), bbox, self.spatial_resolution, "average net building height") - .b1) - + + anbh_ic = ee.ImageCollection(anbh) + data = get_image_collection( + anbh_ic, + 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 ab080f51..aef268e8 100644 --- a/city_metrix/layers/built_up_height.py +++ b/city_metrix/layers/built_up_height.py @@ -1,9 +1,6 @@ -from dask.diagnostics import ProgressBar -import xarray as xr -import xee import ee -from .layer import Layer, get_utm_zone_epsg, get_image_collection +from .layer import Layer, get_image_collection class BuiltUpHeight(Layer): @@ -22,8 +19,15 @@ def get_data(self, bbox): # ANBH is the average height of the built surfaces, USE THIS # AGBH is the amount of built cubic meters per surface unit in the cell # 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.spatial_resolution, "built up height") - return data.built_height + built_height_ic = ee.ImageCollection(built_height) + data = get_image_collection( + built_height_ic, + bbox, + self.spatial_resolution, + "built up height" + ).built_height + + return data diff --git a/city_metrix/layers/esa_world_cover.py b/city_metrix/layers/esa_world_cover.py index a4b0f65c..c2147812 100644 --- a/city_metrix/layers/esa_world_cover.py +++ b/city_metrix/layers/esa_world_cover.py @@ -1,9 +1,7 @@ -from dask.diagnostics import ProgressBar -from enum import Enum -import xarray as xr import ee -from .layer import Layer, get_utm_zone_epsg, get_image_collection +from enum import Enum +from .layer import Layer, get_image_collection class EsaWorldCoverClass(Enum): @@ -39,19 +37,18 @@ def __init__(self, land_cover_class=None, year=2020, spatial_resolution=10, **kw def get_data(self, bbox): if self.year == 2020: - data = get_image_collection( - ee.ImageCollection("ESA/WorldCover/v100"), - bbox, - self.spatial_resolution, - "ESA world cover" - ).Map + esa_data_ic = ee.ImageCollection("ESA/WorldCover/v100") elif self.year == 2021: - data = get_image_collection( - ee.ImageCollection("ESA/WorldCover/v200"), - bbox, - self.spatial_resolution, - "ESA world cover" - ).Map + esa_data_ic = ee.ImageCollection("ESA/WorldCover/v200") + else: + raise ValueError(f'Specified year ({self.year}) is not currently supported') + + data = get_image_collection( + esa_data_ic, + bbox, + self.spatial_resolution, + "ESA world cover" + ).Map if self.land_cover_class: data = data.where(data == self.land_cover_class.value) diff --git a/city_metrix/layers/glad_lulc.py b/city_metrix/layers/glad_lulc.py index 16d32346..11afe318 100644 --- a/city_metrix/layers/glad_lulc.py +++ b/city_metrix/layers/glad_lulc.py @@ -1,7 +1,7 @@ import xarray as xr import ee -from .layer import Layer, get_utm_zone_epsg, get_image_collection +from .layer import Layer, get_image_collection class LandCoverGlad(Layer): @@ -17,8 +17,9 @@ def __init__(self, year=2020, spatial_resolution=30, **kwargs): self.spatial_resolution = spatial_resolution def get_data(self, bbox): + lcluc_ic = ee.ImageCollection(ee.Image(f'projects/glad/GLCLU2020/LCLUC_{self.year}')) data = get_image_collection( - ee.ImageCollection(ee.Image(f'projects/glad/GLCLU2020/LCLUC_{self.year}')), + lcluc_ic, bbox, self.spatial_resolution, "GLAD Land Cover" @@ -80,7 +81,8 @@ def __init__(self, year=2020, spatial_resolution=30, **kwargs): self.spatial_resolution = spatial_resolution def get_data(self, bbox): - simplified_glad = LandCoverSimplifiedGlad(year=self.year, spatial_resolution=self.spatial_resolution).get_data(bbox) + simplified_glad = (LandCoverSimplifiedGlad(year=self.year, spatial_resolution=self.spatial_resolution) + .get_data(bbox)) # Copy the original data data = simplified_glad.copy(deep=True) @@ -108,8 +110,10 @@ def __init__(self, start_year=2000, end_year=2020, spatial_resolution=30, **kwar self.spatial_resolution = spatial_resolution def get_data(self, bbox): - habitat_glad_start = LandCoverHabitatGlad(year=self.start_year, spatial_resolution=self.spatial_resolution).get_data(bbox) - habitat_glad_end = LandCoverHabitatGlad(year=self.end_year, spatial_resolution=self.spatial_resolution).get_data(bbox) + habitat_glad_start = (LandCoverHabitatGlad(year=self.start_year, spatial_resolution=self.spatial_resolution) + .get_data(bbox)) + habitat_glad_end = (LandCoverHabitatGlad(year=self.end_year, spatial_resolution=self.spatial_resolution) + .get_data(bbox)) # Class 01: Became habitat between start year and end year class_01 = ((habitat_glad_start == 0) & (habitat_glad_end == 1)) diff --git a/city_metrix/layers/high_land_surface_temperature.py b/city_metrix/layers/high_land_surface_temperature.py index 5faa2aed..9cbf5eac 100644 --- a/city_metrix/layers/high_land_surface_temperature.py +++ b/city_metrix/layers/high_land_surface_temperature.py @@ -1,11 +1,9 @@ -from .landsat_collection_2 import LandsatCollection2 -from .land_surface_temperature import LandSurfaceTemperature -from .layer import Layer - -from shapely.geometry import box import datetime import ee +from shapely.geometry import box +from .land_surface_temperature import LandSurfaceTemperature +from .layer import Layer class HighLandSurfaceTemperature(Layer): """ @@ -28,6 +26,7 @@ def get_data(self, bbox): end_date = (hottest_date + datetime.timedelta(days=45)).strftime("%Y-%m-%d") lst = LandSurfaceTemperature(start_date, end_date, self.spatial_resolution).get_data(bbox) + lst_mean = lst.mean(dim=['x', 'y']) high_lst = lst.where(lst >= (lst_mean + self.THRESHOLD_ADD)) return high_lst @@ -36,12 +35,17 @@ def get_hottest_date(self, bbox): centroid = box(*bbox).centroid dataset = ee.ImageCollection("ECMWF/ERA5/DAILY") - AirTemperature = (dataset - .filter(ee.Filter.And( - ee.Filter.date(self.start_date, self.end_date), - ee.Filter.bounds(ee.Geometry.BBox(*bbox)))) - .select(['maximum_2m_air_temperature'], ['tasmax']) - ) + + AirTemperature = ( + dataset + .filter( + ee.Filter + .And(ee.Filter.date(self.start_date, self.end_date), + ee.Filter.bounds(ee.Geometry.BBox(*bbox)) + ) + ) + .select(['maximum_2m_air_temperature'], ['tasmax']) + ) # add date as a band to image collection def addDate(image): @@ -56,8 +60,17 @@ def addDate(image): # reduce composite to get the hottest date for centroid of ROI resolution = dataset.first().projection().nominalScale() - hottest_date = str( - ee.Number(hottest.reduceRegion(ee.Reducer.firstNonNull(), ee.Geometry.Point([centroid.x, centroid.y]), resolution).get('date')).getInfo()) + hottest_date = ( + ee.Number( + hottest.reduceRegion(ee.Reducer.firstNonNull(), + ee.Geometry.Point([centroid.x, centroid.y]), + resolution + ).get('date') + ) + .getInfo() + ) # convert to date object - return datetime.datetime.strptime(hottest_date, "%Y%m%d").date() + formated_hottest_data = datetime.datetime.strptime(str(hottest_date), "%Y%m%d").date() + + return formated_hottest_data diff --git a/city_metrix/layers/impervious_surface.py b/city_metrix/layers/impervious_surface.py index ea6772ec..9cf017d9 100644 --- a/city_metrix/layers/impervious_surface.py +++ b/city_metrix/layers/impervious_surface.py @@ -1,9 +1,6 @@ -from dask.diagnostics import ProgressBar -import xarray as xr -import xee import ee -from .layer import Layer, get_utm_zone_epsg, get_image_collection +from .layer import Layer, get_image_collection class ImperviousSurface(Layer): @@ -18,12 +15,20 @@ def __init__(self, spatial_resolution=100, **kwargs): def get_data(self, bbox): # load impervious_surface - dataset = ee.ImageCollection(ee.Image("Tsinghua/FROM-GLC/GAIA/v10").gt(0)) # change_year_index is zero if permeable as of 2018 - imperv_surf = ee.ImageCollection(dataset - .filterBounds(ee.Geometry.BBox(*bbox)) - .select('change_year_index') - .sum() - ) - - data = get_image_collection(imperv_surf, bbox, self.spatial_resolution, "imperv surf") - return data.change_year_index + # change_year_index is zero if permeable as of 2018 + impervious_surface = ee.ImageCollection(ee.Image("Tsinghua/FROM-GLC/GAIA/v10").gt(0)) + + imperv_surf_ic = ee.ImageCollection(impervious_surface + .filterBounds(ee.Geometry.BBox(*bbox)) + .select('change_year_index') + .sum() + ) + + data = get_image_collection( + imperv_surf_ic, + bbox, + self.spatial_resolution, + "imperv surf" + ).change_year_index + + return data diff --git a/city_metrix/layers/land_surface_temperature.py b/city_metrix/layers/land_surface_temperature.py index 931cb2e0..93888ce8 100644 --- a/city_metrix/layers/land_surface_temperature.py +++ b/city_metrix/layers/land_surface_temperature.py @@ -1,9 +1,6 @@ -from .landsat_collection_2 import LandsatCollection2 -from .layer import Layer, get_utm_zone_epsg, get_image_collection - -from dask.diagnostics import ProgressBar import ee -import xarray + +from .layer import Layer, get_image_collection class LandSurfaceTemperature(Layer): """ @@ -22,6 +19,7 @@ def __init__(self, start_date="2013-01-01", end_date="2023-01-01", spatial_resol def get_data(self, bbox): def cloud_mask(image): qa = image.select('QA_PIXEL') + mask = qa.bitwiseAnd(1 << 3).Or(qa.bitwiseAnd(1 << 4)) return image.updateMask(mask.Not()) @@ -30,13 +28,22 @@ def apply_scale_factors(image): return thermal_band l8 = ee.ImageCollection("LANDSAT/LC08/C02/T1_L2") - l8_st = l8 \ - .select('ST_B10', 'QA_PIXEL') \ - .filter(ee.Filter.date(self.start_date, self.end_date)) \ - .filterBounds(ee.Geometry.BBox(*bbox)) \ - .map(cloud_mask) \ - .map(apply_scale_factors) \ - .reduce(ee.Reducer.mean()) - - data = get_image_collection(ee.ImageCollection(l8_st), bbox, self.spatial_resolution, "LST").ST_B10_mean + + l8_st = (l8 + .select('ST_B10', 'QA_PIXEL') + .filter(ee.Filter.date(self.start_date, self.end_date)) + .filterBounds(ee.Geometry.BBox(*bbox)) + .map(cloud_mask) + .map(apply_scale_factors) + .reduce(ee.Reducer.mean()) + ) + + l8_st_ic = ee.ImageCollection(l8_st) + data = get_image_collection( + l8_st_ic, + bbox, + self.spatial_resolution, + "LST" + ).ST_B10_mean + return data diff --git a/city_metrix/layers/layer.py b/city_metrix/layers/layer.py index 83fa9eb2..3d2829fa 100644 --- a/city_metrix/layers/layer.py +++ b/city_metrix/layers/layer.py @@ -2,7 +2,6 @@ from abc import abstractmethod from typing import Union, Tuple from uuid import uuid4 -from osgeo import gdal import ee import boto3 diff --git a/city_metrix/layers/nasa_dem.py b/city_metrix/layers/nasa_dem.py index b5ac45d8..d3840d34 100644 --- a/city_metrix/layers/nasa_dem.py +++ b/city_metrix/layers/nasa_dem.py @@ -1,6 +1,4 @@ import ee -import xee -import xarray as xr from .layer import Layer, get_image_collection @@ -16,12 +14,20 @@ def __init__(self, spatial_resolution=30, **kwargs): self.spatial_resolution = spatial_resolution def get_data(self, bbox): - dataset = ee.Image("NASA/NASADEM_HGT/001") - nasa_dem = ee.ImageCollection(ee.ImageCollection(dataset) - .filterBounds(ee.Geometry.BBox(*bbox)) - .select('elevation') - .mean() - ) - data = get_image_collection(nasa_dem, bbox, self.spatial_resolution, "NASA DEM").elevation + nasa_dem = ee.Image("NASA/NASADEM_HGT/001") + + nasa_dem_elev = (ee.ImageCollection(nasa_dem) + .filterBounds(ee.Geometry.BBox(*bbox)) + .select('elevation') + .mean() + ) + + nasa_dem_elev_ic = ee.ImageCollection(nasa_dem_elev) + data = get_image_collection( + nasa_dem_elev_ic, + 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 5efe4a8e..fdf499b3 100644 --- a/city_metrix/layers/natural_areas.py +++ b/city_metrix/layers/natural_areas.py @@ -1,4 +1,3 @@ -import xarray as xr from xrspatial.classify import reclassify from .layer import Layer @@ -32,7 +31,11 @@ def get_data(self, bbox): } # Perform the reclassification - reclassified_data = reclassify(esa_world_cover, bins=list(reclass_map.keys()), new_values=list(reclass_map.values())) + reclassified_data = reclassify( + esa_world_cover, + bins=list(reclass_map.keys()), + new_values=list(reclass_map.values()) + ) # Apply the original CRS (Coordinate Reference System) reclassified_data = reclassified_data.rio.write_crs(esa_world_cover.rio.crs, inplace=True) diff --git a/city_metrix/layers/ndvi_sentinel2_gee.py b/city_metrix/layers/ndvi_sentinel2_gee.py index fce30219..b31021dc 100644 --- a/city_metrix/layers/ndvi_sentinel2_gee.py +++ b/city_metrix/layers/ndvi_sentinel2_gee.py @@ -1,4 +1,5 @@ import ee + from .layer import Layer, get_image_collection class NdviSentinel2(Layer): @@ -32,6 +33,7 @@ def calculate_ndvi(image): return image.addBands(ndvi) s2 = ee.ImageCollection("COPERNICUS/S2_HARMONIZED") + ndvi = (s2 .filterBounds(ee.Geometry.BBox(*bbox)) .filterDate(start_date, end_date) @@ -41,8 +43,11 @@ def calculate_ndvi(image): ndvi_mosaic = ndvi.qualityMosaic('NDVI') - ic = ee.ImageCollection(ndvi_mosaic) - ndvi_data = (get_image_collection(ic, bbox, self.spatial_resolution, "NDVI") - .NDVI) + ndvi_mosaic_ic = ee.ImageCollection(ndvi_mosaic) + ndvi_data = get_image_collection( + ndvi_mosaic_ic, + bbox, + self.spatial_resolution, "NDVI" + ).NDVI return ndvi_data diff --git a/city_metrix/layers/smart_surface_lulc.py b/city_metrix/layers/smart_surface_lulc.py index e4759a69..2e0f0839 100644 --- a/city_metrix/layers/smart_surface_lulc.py +++ b/city_metrix/layers/smart_surface_lulc.py @@ -1,9 +1,7 @@ import xarray as xr import numpy as np import pandas as pd -import geopandas as gpd from shapely.geometry import CAP_STYLE, JOIN_STYLE -from shapely.geometry import box from exactextract import exact_extract from geocube.api.core import make_geocube import warnings @@ -20,15 +18,16 @@ class SmartSurfaceLULC(Layer): - def __init__(self, land_cover_class=None, **kwargs): + def __init__(self, land_cover_class=None, spatial_resolution=10, **kwargs): super().__init__(**kwargs) self.land_cover_class = land_cover_class + self.spatial_resolution = spatial_resolution def get_data(self, bbox): crs = get_utm_zone_epsg(bbox) # ESA world cover - esa_world_cover = EsaWorldCover(year=2021).get_data(bbox) + esa_world_cover = EsaWorldCover(year=2021, spatial_resolution = self.spatial_resolution).get_data(bbox) # ESA reclass and upsample def get_data_esa_reclass(esa_world_cover): reclass_map = { @@ -46,7 +45,11 @@ def get_data_esa_reclass(esa_world_cover): } # Perform the reclassification - reclassified_esa = reclassify(esa_world_cover, bins=list(reclass_map.keys()), new_values=list(reclass_map.values())) + reclassified_esa = reclassify( + esa_world_cover, + bins=list(reclass_map.keys()), + new_values=list(reclass_map.values()) + ) esa_1m = reclassified_esa.rio.reproject( dst_crs=crs, @@ -74,7 +77,8 @@ def get_data_esa_reclass(esa_world_cover): if len(roads_osm) > 0: roads_osm['lanes'] = pd.to_numeric(roads_osm['lanes'], errors='coerce') # Get the average number of lanes per highway class - lanes = (roads_osm.drop(columns='geometry') + lanes = (roads_osm + .drop(columns='geometry') .groupby('highway') # Calculate average and round up .agg(avg_lanes=('lanes', lambda x: np.ceil(np.nanmean(x)) if not np.isnan(x).all() else np.NaN)) @@ -93,11 +97,16 @@ def get_data_esa_reclass(esa_world_cover): # https://nacto.org/publication/urban-street-design-guide/street-design-elements/lane-width/#:~:text=wider%20lane%20widths.-,Lane%20widths%20of%2010%20feet%20are%20appropriate%20in%20urban%20areas,be%20used%20in%20each%20direction # cap is flat to the terminus of the road # join style is mitred so intersections are squared - roads_osm['geometry'] = roads_osm.apply(lambda row: row['geometry'].buffer( - row['lanes'] * 3.048 / 2, - cap_style=CAP_STYLE.flat, - join_style=JOIN_STYLE.mitre), - axis=1 + roads_osm['geometry'] = ( + roads_osm + .apply( + lambda row: row['geometry'] + .buffer( + row['lanes'] * 3.048 / 2, + cap_style=CAP_STYLE.flat, + join_style=JOIN_STYLE.mitre + ),axis=1 + ) ) else: # Add value field (30) @@ -179,14 +188,20 @@ def classify_building(building): if len(buildings) > 0: buildings['Value'] = buildings.apply(classify_building, axis=1) - # Parking parking_osm = OpenStreetMap(osm_class=OpenStreetMapClass.PARKING).get_data(bbox).reset_index() parking_osm['Value'] = 50 - # combine features: open space, water, road, building, parking - feature_df = pd.concat([open_space_osm[['geometry', 'Value']], water_osm[['geometry', 'Value']], roads_osm[['geometry', 'Value']], buildings[['geometry', 'Value']], parking_osm[['geometry', 'Value']]], axis=0) + feature_df = pd.concat( + [open_space_osm[['geometry', 'Value']], + water_osm[['geometry', 'Value']], + roads_osm[['geometry', 'Value']], + buildings[['geometry', 'Value']], + parking_osm[['geometry', 'Value']] + ], axis=0 + ) + # rasterize if feature_df.empty: feature_1m = xr.zeros_like(esa_1m) diff --git a/city_metrix/layers/tree_canopy_height.py b/city_metrix/layers/tree_canopy_height.py index fee1697b..f499552b 100644 --- a/city_metrix/layers/tree_canopy_height.py +++ b/city_metrix/layers/tree_canopy_height.py @@ -1,10 +1,7 @@ -from .layer import Layer, get_utm_zone_epsg, get_image_collection - -from dask.diagnostics import ProgressBar -import xarray as xr -import xee import ee +from .layer import Layer, get_image_collection + class TreeCanopyHeight(Layer): """ Attributes: @@ -20,10 +17,19 @@ def __init__(self, spatial_resolution=1, **kwargs): def get_data(self, bbox): canopy_ht = ee.ImageCollection("projects/meta-forest-monitoring-okw37/assets/CanopyHeight") + # aggregate time series into a single image - canopy_ht = canopy_ht.reduce(ee.Reducer.mean()).rename("cover_code") + canopy_ht_img = (canopy_ht + .reduce(ee.Reducer.mean()) + .rename("cover_code") + ) - data = get_image_collection(ee.ImageCollection(canopy_ht), bbox, - self.spatial_resolution, "tree canopy height") + canopy_ht_ic = ee.ImageCollection(canopy_ht_img) + data = get_image_collection( + canopy_ht_ic, + bbox, + self.spatial_resolution, + "tree canopy height" + ).cover_code - return data.cover_code + return data diff --git a/city_metrix/layers/tree_cover.py b/city_metrix/layers/tree_cover.py index 98bc481d..52fc8d6c 100644 --- a/city_metrix/layers/tree_cover.py +++ b/city_metrix/layers/tree_cover.py @@ -1,10 +1,7 @@ -from .layer import Layer, get_utm_zone_epsg, get_image_collection - -from dask.diagnostics import ProgressBar -import xarray as xr -import xee import ee +from .layer import Layer, get_image_collection + class TreeCover(Layer): """ Merged tropical and nontropical tree cover from WRI @@ -24,11 +21,21 @@ def __init__(self, min_tree_cover=None, max_tree_cover=None, spatial_resolution= def get_data(self, bbox): tropics = ee.ImageCollection('projects/wri-datalab/TropicalTreeCover') - nontropics = ee.ImageCollection('projects/wri-datalab/TTC-nontropics') - 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.spatial_resolution, "tree cover").ttc + non_tropics = ee.ImageCollection('projects/wri-datalab/TTC-nontropics') + + merged_ttc = tropics.merge(non_tropics) + ttc_image = (merged_ttc + .reduce(ee.Reducer.mean()) + .rename('ttc') + ) + + ttc_ic = ee.ImageCollection(ttc_image) + data = get_image_collection( + ttc_ic, + bbox, + self.spatial_resolution, + "tree cover" + ).ttc if self.min_tree_cover is not None: data = data.where(data >= self.min_tree_cover) @@ -36,4 +43,3 @@ def get_data(self, bbox): data = data.where(data <= self.max_tree_cover) return data - diff --git a/city_metrix/layers/urban_land_use.py b/city_metrix/layers/urban_land_use.py index fe69c758..02c737cb 100644 --- a/city_metrix/layers/urban_land_use.py +++ b/city_metrix/layers/urban_land_use.py @@ -1,9 +1,6 @@ -from dask.diagnostics import ProgressBar -import xarray as xr -import xee import ee -from .layer import Layer, get_utm_zone_epsg, get_image_collection +from .layer import Layer, get_image_collection class UrbanLandUse(Layer): @@ -19,21 +16,28 @@ def __init__(self, band='lulc', spatial_resolution=5, **kwargs): self.spatial_resolution = spatial_resolution def get_data(self, bbox): - dataset = ee.ImageCollection("projects/wri-datalab/cities/urban_land_use/V1") + ulu = ee.ImageCollection("projects/wri-datalab/cities/urban_land_use/V1") + # ImageCollection didn't cover the globe - if dataset.filterBounds(ee.Geometry.BBox(*bbox)).size().getInfo() == 0: - ulu = ee.ImageCollection(ee.Image.constant(0) + if ulu.filterBounds(ee.Geometry.BBox(*bbox)).size().getInfo() == 0: + ulu_ic = ee.ImageCollection(ee.Image + .constant(0) .clip(ee.Geometry.BBox(*bbox)) .rename('lulc') ) else: - ulu = ee.ImageCollection(dataset + ulu_ic = ee.ImageCollection(ulu .filterBounds(ee.Geometry.BBox(*bbox)) .select(self.band) .reduce(ee.Reducer.firstNonNull()) .rename('lulc') ) - data = get_image_collection(ulu, bbox, self.spatial_resolution, "urban land use").lulc + data = get_image_collection( + ulu_ic, + 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 30fb6d8d..700010a3 100644 --- a/city_metrix/layers/world_pop.py +++ b/city_metrix/layers/world_pop.py @@ -1,9 +1,6 @@ -from dask.diagnostics import ProgressBar -import xarray as xr -import xee import ee -from .layer import Layer, get_utm_zone_epsg, get_image_collection +from .layer import Layer, get_image_collection class WorldPop(Layer): @@ -27,23 +24,44 @@ def get_data(self, bbox): if not self.agesex_classes: # total population dataset = ee.ImageCollection('WorldPop/GP/100m/pop') - world_pop = ee.ImageCollection(dataset - .filterBounds(ee.Geometry.BBox(*bbox)) - .filter(ee.Filter.inList('year', [self.year])) - .select('population') - .mean() - ) - data = get_image_collection(world_pop, bbox, self.spatial_resolution, "world pop").population + world_pop_ic = ee.ImageCollection( + dataset + .filterBounds(ee.Geometry.BBox(*bbox)) + .filter(ee.Filter.inList('year', [self.year])) + .select('population') + .mean() + ) + + data = get_image_collection( + world_pop_ic, + bbox, + self.spatial_resolution, + "world pop" + ).population else: # sum population for selected age-sex groups - dataset = ee.ImageCollection('WorldPop/GP/100m/pop_age_sex') - world_pop = dataset.filterBounds(ee.Geometry.BBox(*bbox))\ - .filter(ee.Filter.inList('year', [self.year]))\ - .select(self.agesex_classes)\ - .mean() - world_pop = ee.ImageCollection(world_pop.reduce(ee.Reducer.sum()).rename('sum_age_sex_group')) - data = get_image_collection(world_pop, bbox, self.spatial_resolution, "world pop age sex").sum_age_sex_group + world_pop_age_sex = ee.ImageCollection('WorldPop/GP/100m/pop_age_sex') + + world_pop_age_sex_year = (world_pop_age_sex + .filterBounds(ee.Geometry.BBox(*bbox)) + .filter(ee.Filter.inList('year', [self.year])) + .select(self.agesex_classes) + .mean() + ) + + world_pop_group_ic = ee.ImageCollection( + world_pop_age_sex_year + .reduce(ee.Reducer.sum()) + .rename('sum_age_sex_group') + ) + + data = get_image_collection( + world_pop_group_ic, + bbox, + self.spatial_resolution, + "world pop age sex" + ).sum_age_sex_group return data diff --git a/tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py b/tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py index d72876d7..22937935 100644 --- a/tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py +++ b/tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py @@ -10,7 +10,7 @@ # RUN_DUMPS is the master control for whether the writes and tests are executed # Setting RUN_DUMPS to True turns on code execution. # Values should normally be set to False in order to avoid unnecessary execution. -RUN_DUMPS = False +RUN_DUMPS = True # Multiplier applied to the default spatial_resolution of the layer # Use value of 1 for default resolution. @@ -24,18 +24,13 @@ CUSTOM_DUMP_DIRECTORY = None def pytest_configure(config): - qgis_project_file = 'layers_for_br_lauro_de_freitas.qgz' if RUN_DUMPS is True: source_folder = os.path.dirname(__file__) target_folder = get_target_folder_path() - create_target_folder(target_folder, True) + create_target_folder(target_folder, False) - source_qgis_file = os.path.join(source_folder, qgis_project_file) - target_qgis_file = os.path.join(target_folder, qgis_project_file) - shutil.copyfile(source_qgis_file, target_qgis_file) - - print("\n\033[93m QGIS project file and layer files written to folder %s.\033[0m\n" % target_folder) + print("\n\033[93m Layer files written to folder %s.\033[0m\n" % target_folder) @pytest.fixture def target_folder(): diff --git a/tests/resources/layer_dumps_for_br_lauro_de_freitas/layers_for_br_lauro_de_freitas.qgz b/tests/resources/layer_dumps_for_br_lauro_de_freitas/layers_for_br_lauro_de_freitas.qgz deleted file mode 100644 index 1cc53ce2df38d8b5e7bd55634a44fd0b186de69d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25729 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 Nr_acf%rpIM{U14Q=Mew^ diff --git a/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_to_qgis_files.py b/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_all_layers.py similarity index 81% rename from tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_to_qgis_files.py rename to tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_all_layers.py index 9c382116..2be8c4df 100644 --- a/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_to_qgis_files.py +++ b/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_all_layers.py @@ -2,27 +2,7 @@ # Execution configuration is specified in the conftest file import pytest -from city_metrix.layers import ( - Albedo, - AlosDSM, - AverageNetBuildingHeight, - EsaWorldCover, - HighLandSurfaceTemperature, - LandsatCollection2, - LandSurfaceTemperature, - NasaDEM, - NaturalAreas, - OpenBuildings, - OpenStreetMap, - OvertureBuildings, - Sentinel2Level2, - NdviSentinel2, - SmartSurfaceLULC, - TreeCanopyHeight, - TreeCover, - UrbanLandUse, - WorldPop, Layer, ImperviousSurface -) +from city_metrix.layers import * from .conftest import RUN_DUMPS, prep_output_path, verify_file_is_populated, get_file_count_in_folder from ...tools.general_tools import get_class_default_spatial_resolution @@ -34,30 +14,6 @@ def test_write_albedo(target_folder, bbox_info, target_spatial_resolution_multip Albedo(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) assert verify_file_is_populated(file_path) -@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') -def test_write_albedo_tiled_unbuffered(target_folder, bbox_info, target_spatial_resolution_multiplier): - file_path = prep_output_path(target_folder, 'albedo_tiled.tif') - target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(Albedo()) - (Albedo(spatial_resolution=target_resolution). - write(bbox_info.bounds, file_path, tile_degrees=0.01, buffer_size=None)) - file_count = get_file_count_in_folder(file_path) - - expected_file_count = 5 # includes 4 tiles and one geojson file - assert file_count == expected_file_count - -@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') -def test_write_albedo_tiled_buffered(target_folder, bbox_info, target_spatial_resolution_multiplier): - buffer_degrees = 0.001 - file_path = prep_output_path(target_folder, 'albedo_tiled_buffered.tif') - target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(Albedo()) - (Albedo(spatial_resolution=target_resolution). - write(bbox_info.bounds, file_path, tile_degrees=0.01, buffer_size=buffer_degrees)) - file_count = get_file_count_in_folder(file_path) - - expected_file_count = 6 # includes 4 tiles and two geojson files - assert file_count == expected_file_count - - @pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') def test_write_alos_dsm(target_folder, bbox_info, target_spatial_resolution_multiplier): file_path = prep_output_path(target_folder, 'alos_dsm.tif') @@ -72,6 +28,13 @@ def test_write_average_net_building_height(target_folder, bbox_info, target_spat AverageNetBuildingHeight(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) assert verify_file_is_populated(file_path) +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_built_up_height(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, 'built_up_height.tif') + target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(BuiltUpHeight()) + BuiltUpHeight(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + @pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') def test_write_esa_world_cover(target_folder, bbox_info, target_spatial_resolution_multiplier): file_path = prep_output_path(target_folder, 'esa_world_cover.tif') @@ -79,6 +42,13 @@ def test_write_esa_world_cover(target_folder, bbox_info, target_spatial_resoluti EsaWorldCover(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) assert verify_file_is_populated(file_path) +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_glad_lulc(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, 'glad_lulc.tif') + target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(LandCoverGlad()) + LandCoverGlad(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + @pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') def test_write_high_land_surface_temperature(target_folder, bbox_info, target_spatial_resolution_multiplier): file_path = prep_output_path(target_folder, 'high_land_surface_temperature.tif') @@ -90,14 +60,7 @@ def test_write_high_land_surface_temperature(target_folder, bbox_info, target_sp def test_write_impervious_surface(target_folder, bbox_info, target_spatial_resolution_multiplier): file_path = prep_output_path(target_folder, 'impervious_surface.tif') target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(ImperviousSurface()) - LandSurfaceTemperature(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) - assert verify_file_is_populated(file_path) - -@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') -def test_write_land_surface_temperature(target_folder, bbox_info, target_spatial_resolution_multiplier): - file_path = prep_output_path(target_folder, 'land_surface_temperature.tif') - target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(LandSurfaceTemperature()) - LandSurfaceTemperature(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) + ImperviousSurface(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) assert verify_file_is_populated(file_path) # TODO Class is no longer used, but may be useful later @@ -158,9 +121,9 @@ def test_write_overture_buildings(target_folder, bbox_info, target_spatial_resol @pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') def test_write_smart_surface_lulc(target_folder, bbox_info, target_spatial_resolution_multiplier): - # Note: spatial_resolution not implemented for this raster class file_path = prep_output_path(target_folder, 'smart_surface_lulc.tif') - SmartSurfaceLULC().write(bbox_info.bounds, file_path, tile_degrees=None) + target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(SmartSurfaceLULC()) + SmartSurfaceLULC(spatial_resolution=target_resolution).write(bbox_info.bounds, file_path, tile_degrees=None) assert verify_file_is_populated(file_path) @pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') diff --git a/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_other.py b/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_other.py new file mode 100644 index 00000000..d4b0a23b --- /dev/null +++ b/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_other.py @@ -0,0 +1,31 @@ +# This code is mostly intended for manual execution +# Execution configuration is specified in the conftest file +import pytest + +from city_metrix.layers import * +from .conftest import RUN_DUMPS, prep_output_path, verify_file_is_populated, get_file_count_in_folder +from ...tools.general_tools import get_class_default_spatial_resolution + + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_albedo_tiled_unbuffered(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, 'albedo_tiled.tif') + target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(Albedo()) + (Albedo(spatial_resolution=target_resolution). + write(bbox_info.bounds, file_path, tile_degrees=0.01, buffer_size=None)) + file_count = get_file_count_in_folder(file_path) + + expected_file_count = 5 # includes 4 tiles and one geojson file + assert file_count == expected_file_count + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_albedo_tiled_buffered(target_folder, bbox_info, target_spatial_resolution_multiplier): + buffer_degrees = 0.001 + file_path = prep_output_path(target_folder, 'albedo_tiled_buffered.tif') + target_resolution = target_spatial_resolution_multiplier * get_class_default_spatial_resolution(Albedo()) + (Albedo(spatial_resolution=target_resolution). + write(bbox_info.bounds, file_path, tile_degrees=0.01, buffer_size=buffer_degrees)) + file_count = get_file_count_in_folder(file_path) + + expected_file_count = 6 # includes 4 tiles and two geojson files + assert file_count == expected_file_count \ No newline at end of file diff --git a/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_using_fixed_resolution.py b/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_using_fixed_resolution.py new file mode 100644 index 00000000..8964b86b --- /dev/null +++ b/tests/resources/layer_dumps_for_br_lauro_de_freitas/test_write_layers_using_fixed_resolution.py @@ -0,0 +1,111 @@ +# This code is mostly intended for manual execution +# Execution configuration is specified in the conftest file +import pytest + +from city_metrix.layers import * +from .conftest import RUN_DUMPS, prep_output_path, verify_file_is_populated, get_file_count_in_folder + +TARGET_RESOLUTION = 5 + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_albedo_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'albedo_{TARGET_RESOLUTION}m.tif') + Albedo(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_alos_dsm_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'alos_dsm_{TARGET_RESOLUTION}m.tif') + AlosDSM(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_average_net_building_height_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'average_net_building_height_{TARGET_RESOLUTION}m.tif') + AverageNetBuildingHeight(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_built_up_height_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'built_up_height_{TARGET_RESOLUTION}m.tif') + BuiltUpHeight(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_esa_world_cover_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'esa_world_cover_{TARGET_RESOLUTION}m.tif') + EsaWorldCover(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_glad_lulc_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'glad_lulc_{TARGET_RESOLUTION}m.tif') + LandCoverGlad(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_high_land_surface_temperature_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'high_land_surface_temperature_{TARGET_RESOLUTION}m.tif') + HighLandSurfaceTemperature(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_impervious_surface_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'impervious_surface_{TARGET_RESOLUTION}m.tif') + ImperviousSurface(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_land_surface_temperature_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'land_surface_temperature_{TARGET_RESOLUTION}m.tif') + LandSurfaceTemperature(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_nasa_dem_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'nasa_dem_{TARGET_RESOLUTION}m.tif') + NasaDEM(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_natural_areas_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'natural_areas_{TARGET_RESOLUTION}m.tif') + NaturalAreas(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_ndvi_sentinel2_gee_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'ndvi_sentinel2_gee_{TARGET_RESOLUTION}m.tif') + NdviSentinel2(year=2023, spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_smart_surface_lulc_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'smart_surface_lulc_{TARGET_RESOLUTION}m.tif') + SmartSurfaceLULC(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_tree_canopy_height_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'tree_canopy_height_{TARGET_RESOLUTION}m.tif') + TreeCanopyHeight(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_tree_cover_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'tree_cover_{TARGET_RESOLUTION}m.tif') + TreeCover(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_urban_land_use_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'urban_land_use_{TARGET_RESOLUTION}m.tif') + UrbanLandUse(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + +@pytest.mark.skipif(RUN_DUMPS == False, reason='Skipping since RUN_DUMPS set to False') +def test_write_world_pop_fixed_res(target_folder, bbox_info, target_spatial_resolution_multiplier): + file_path = prep_output_path(target_folder, f'world_pop_{TARGET_RESOLUTION}m.tif') + WorldPop(spatial_resolution=TARGET_RESOLUTION).write(bbox_info.bounds, file_path, tile_degrees=None) + assert verify_file_is_populated(file_path) + From f9f4e12d099be54b87a32b08d5e1d39fb8f308bc Mon Sep 17 00:00:00 2001 From: Kenn Cartier Date: Fri, 13 Dec 2024 12:36:30 -0800 Subject: [PATCH 2/3] Turned off testing for layer writes --- tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py b/tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py index 22937935..6b33b516 100644 --- a/tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py +++ b/tests/resources/layer_dumps_for_br_lauro_de_freitas/conftest.py @@ -10,7 +10,7 @@ # RUN_DUMPS is the master control for whether the writes and tests are executed # Setting RUN_DUMPS to True turns on code execution. # Values should normally be set to False in order to avoid unnecessary execution. -RUN_DUMPS = True +RUN_DUMPS = False # Multiplier applied to the default spatial_resolution of the layer # Use value of 1 for default resolution. From 0c8b93e2e703200f4647b23dbf6347090ecb9e0a Mon Sep 17 00:00:00 2001 From: Kenn Cartier Date: Fri, 13 Dec 2024 15:26:52 -0800 Subject: [PATCH 3/3] Dependency updates to fix github Actions failure --- environment.yml | 1 + tests/test_layer_parameters.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 03501138..39537969 100644 --- a/environment.yml +++ b/environment.yml @@ -6,6 +6,7 @@ dependencies: - earthengine-api=0.1.411 - geocube=0.4.2 - geopandas=0.14.4 + - xarray=2024.7.0 - rioxarray=0.15.0 - odc-stac=0.3.8 - pystac-client=0.7.5 diff --git a/tests/test_layer_parameters.py b/tests/test_layer_parameters.py index 3fe415be..359bb287 100644 --- a/tests/test_layer_parameters.py +++ b/tests/test_layer_parameters.py @@ -1,4 +1,5 @@ import pytest +import xarray as xr import numpy as np from skimage.metrics import structural_similarity as ssim from pyproj import CRS @@ -286,7 +287,10 @@ def _get_sample_data(class_instance, bbox, downsize_factor): return default_res_data, downsized_res_data, downsized_resolution, estimated_actual_resolution def _get_crs_from_image_data(image_data): - crs_string = image_data.rio.crs.data['init'] + if isinstance(image_data, xr.DataArray): + crs_string = image_data.rio.crs.wkt + else: + crs_string = image_data.crs crs = CRS.from_string(crs_string) return crs