From 2d4c9fc911cc54d009c1948386f9aeb77a6a824e Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Wed, 23 Oct 2024 18:48:16 +0000 Subject: [PATCH] fix: accept ee.Reducer and str for everything needing a reducer parameter --- geetools/Image.py | 27 ++--- geetools/ImageCollection.py | 109 ++++++++++-------- tests/test_ImageCollection.py | 8 -- .../test_reduce_interval_quality_mosaic.csv | 2 - 4 files changed, 75 insertions(+), 71 deletions(-) delete mode 100644 tests/test_ImageCollection/test_reduce_interval_quality_mosaic.csv diff --git a/geetools/Image.py b/geetools/Image.py index d8608852..dd31ae72 100644 --- a/geetools/Image.py +++ b/geetools/Image.py @@ -530,7 +530,7 @@ def fullLike( def reduceBands( self, - reducer: str, + reducer: str | ee.Reducer, bands: list | ee.List = [], name: str | ee.String = "", ) -> ee.Image: @@ -562,7 +562,8 @@ def reduceBands( bands, name = ee.List(bands), ee.String(name) bands = ee.Algorithms.If(bands.size().eq(0), self._obj.bandNames(), bands) name = ee.Algorithms.If(name.equals(ee.String("")), reducer, name) - reduceImage = self._obj.select(ee.List(bands)).reduce(reducer).rename([name]) + red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer + reduceImage = self._obj.select(ee.List(bands)).reduce(red).rename([name]) return self._obj.addBands(reduceImage) def negativeClip(self, geometry: ee.Geometry | ee.Feature | ee.FeatureCollection) -> ee.Image: @@ -1422,7 +1423,7 @@ def plot( def byBands( self, regions: ee.featurecollection, - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, bands: list = [], regionId: str = "system:index", @@ -1442,7 +1443,7 @@ def byBands( Parameters: regions: The regions to compute the reducer in. - reducer: The name of the reducer to use, default to "mean". + reducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale to use for the computation. Default is 10000m. regionId: The property used to label region. Defaults to "system:index". labels: The labels to use for the output dictionary. Default to the band names. @@ -1484,7 +1485,7 @@ def byBands( # This is currently hidden because of https://issuetracker.google.com/issues/374285504 # It will have no impact on most of the cases as plt_hist should be used for single band images # reducer = reducer.setOutputs(labels) - red = getattr(ee.Reducer, reducer)() + red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer # retrieve the reduce bands for each feature image = self._obj.select(eeBands).rename(eeLabels) @@ -1499,7 +1500,7 @@ def byBands( def byRegions( self, regions: ee.featurecollection, - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, bands: list = [], regionId: str = "system:index", @@ -1519,7 +1520,7 @@ def byRegions( Parameters: regions: The regions to compute the reducer in. - reducer: The name of the reducer to use, default to "mean". + reducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale to use for the computation. Default is 10000m. regionId: The property used to label region. Defaults to "system:index". labels: The labels to use for the output dictionary. Default to the band names. @@ -1561,11 +1562,11 @@ def byRegions( # This is currently hidden because of https://issuetracker.google.com/issues/374285504 # It will have no impact on most of the cases as plt_hist should be used for single band images # reducer = reducer.setOutputs(labels) - red = getattr(ee.Reducer, reducer)() + red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer # retrieve the reduce bands for each feature image = self._obj.select(bands).rename(labels) - fc = image.reduceRegions(collection=regions, reducer=red, scale=scale) + fc = image.reduceRegions(regions, red, scale) # extract the data as a list of dictionaries (one for each label) aggregating # we are force to turn the fc into a list because GEE don't accept to map a featureCollection @@ -1579,7 +1580,7 @@ def plot_by_regions( self, type: str, regions: ee.FeatureCollection, - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, bands: list = [], regionId: str = "system:index", @@ -1599,7 +1600,7 @@ def plot_by_regions( Parameters: type: The type of plot to use. Defaults to "bar". can be any type of plot from the python lib `matplotlib.pyplot`. If the one you need is missing open an issue! regions: The regions to compute the reducer in. - reducer: The name of the reducer to use, default to "mean". + rreducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale to use for the computation. Default is 10000m. bands: The bands to compute the reducer on. Default to all bands. regionId: The property used to label region. Defaults to "system:index". @@ -1649,7 +1650,7 @@ def plot_by_bands( self, type: str, regions: ee.FeatureCollection, - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, bands: list = [], regionId: str = "system:index", @@ -1670,7 +1671,7 @@ def plot_by_bands( Parameters: type: The type of plot to use. Defaults to "bar". can be any type of plot from the python lib `matplotlib.pyplot`. If the one you need is missing open an issue! regions: The regions to compute the reducer in. - reducer: The name of the reducer to use, default to "mean". + reducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale to use for the computation. Default is 10000m. bands: The bands to compute the reducer on. Default to all bands. regionId: The property used to label region. Defaults to "system:index". diff --git a/geetools/ImageCollection.py b/geetools/ImageCollection.py index 9cb079ec..354d9611 100644 --- a/geetools/ImageCollection.py +++ b/geetools/ImageCollection.py @@ -899,7 +899,10 @@ def groupInterval(self, unit: str = "month", duration: int = 1) -> ee.List: return ee.List(imageCollectionList) def reduceInterval( - self, reducer: str = "mean", unit: str = "month", duration: int = 1, qualityBand: str = "" + self, + reducer: str | ee.Reducer = "mean", + unit: str = "month", + duration: int = 1, ) -> ee.ImageCollection: """Reduce the images included in the same duration interval using the provided reducer. @@ -909,10 +912,9 @@ def reduceInterval( processed. Args: - reducer: The reducer to use. Default is "mean". Available reducers: "mean", "median", "max", "min", "sum", "stdDev", "count", "product", "first", "mosaic", "qualityMosaic" or "last". + reducer: The name of the reducer to use or a Reducer object. Default is "mean". unit: The unit of time to split the collection. Available units: 'year', 'month', 'week', 'day', 'hour', 'minute' or 'second'. duration: The duration of each split. - qualityBand: The band to use as quality band. Only available for "qualityMosaic" reducer. Returns: A new ImageCollection with the reduced images. @@ -937,21 +939,17 @@ def reduceInterval( # Every subcollection is sorted in case one use the "first" reducer imageCollectionList = self.groupInterval(unit, duration) + # create a reducer from user parameters + red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer + def reduce(ic): timeList = ee.ImageCollection(ic).aggregate_array("system:time_start") start, end = timeList.get(0), timeList.get(-1) - reduced = getattr(ee.ImageCollection(ic), reducer) - image = reduced(qualityBand) if reducer == "qualityMosaic" else reduced() + bandNames = ee.ImageCollection(ic).first().bandNames() + image = ee.ImageCollection(ic).reduce(red).rename(bandNames) return image.set("system:time_start", start, "system:time_end", end) - # catch the error if the reducer is not available in the ee.ImageCollection class - # and provide a more meaningful error message. - try: - reducedImagesList = imageCollectionList.map(reduce) - except AttributeError: - raise AttributeError( - f'Reducer "{reducer}" not available in the ee.ImageCollection class' - ) + reducedImagesList = imageCollectionList.map(reduce) # set back the original properties ic = ee.ImageCollection(reducedImagesList).copyProperties(self._obj) @@ -1062,7 +1060,7 @@ def computeDistance(other): def datesByBands( self, region: ee.Geometry, - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", bands: list = [], @@ -1082,7 +1080,7 @@ def datesByBands( Parameters: region: The region to reduce the data on. - reducer: The name of the reducer to use. Default is "mean". + reducer: The name of the reducer or a reducer object use. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". bands: The bands to reduce. If empty, all bands are reduced. @@ -1118,10 +1116,13 @@ def datesByBands( # aggregate all the dates contained in the collection dateList = ic.aggregate_array(dateProperty).map(lambda d: ee.Date(d).format(EE_DATE_FORMAT)) + # create a reducer from the specified parameters + red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer + # create a list of dictionaries with the reduced values for each band def reduce(lbl: ee.String) -> ee.Dictionary: image = ic.select([lbl]).toBands().rename(dateList) - return image.reduceRegion(reducer, region, scale) + return image.reduceRegion(red, region, scale) return ee.Dictionary.fromLists(eeLabels, eeLabels.map(reduce)) @@ -1130,7 +1131,7 @@ def datesByRegions( band: str, regions: ee.FeatureCollection, label: str = "system:index", - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", ) -> ee.Dictionary: @@ -1150,7 +1151,7 @@ def datesByRegions( band: The band to reduce. regions: The regions to reduce the data on. label: The property to use as label for each region. Default is "system:index". - reducer: The name of the reducer to use. Default is "mean". + reducer: The name of the reducer or a reducer object use. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". @@ -1186,7 +1187,7 @@ def to_string(date: ee.Date) -> ee.String: # reduce the data for each region image = self._obj.select([band]).toBands().rename(dateList) - red = getattr(ee.Reducer, reducer)() + red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer reduced = image.reduceRegions(regions, red, scale) # create a list of dictionaries for each region and aggregate them into a dictionary @@ -1198,8 +1199,8 @@ def to_string(date: ee.Date) -> ee.String: def doyByBands( self, region: ee.Geometry, - spatialReducer: str = "mean", - timeReducer: str = "mean", + spatialReducer: str | ee.Reducer = "mean", + timeReducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", bands: list = [], @@ -1219,8 +1220,8 @@ def doyByBands( Parameters: region: The region to reduce the data on. - spatialReducer: The name of the reducer to use. Default is "mean". - timeReducer: The name of the reducer to use for the temporal reduction. Default is "mean". + spatialReducer: The name of the reducer or a reducer object to use for spatial reduction. Default is "mean". + timeReducer: The name of the reducer or a reducer object to use for time reduction. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". bands: The bands to reduce. If empty, all bands are reduced. @@ -1258,7 +1259,9 @@ def filter_doy(d: ee.Number) -> ee.ImageCollection: # reduce every sub ImageCollection in the list into images (it's the temporal reduction) # and aggregate the result as a single ImageCollection - timeRed = getattr(ee.Reducer, timeReducer)() # .setOutputs(labels) + timeRed = ( + getattr(ee.Reducer, timeReducer)() if isinstance(timeReducer, str) else timeReducer + ) def timeReduce(c: ee.imageCollection) -> ee.image: c = ee.ImageCollection(c) @@ -1270,7 +1273,11 @@ def timeReduce(c: ee.imageCollection) -> ee.image: # spatially reduce the generated imagecollection over the region for each band doyList = ic.aggregate_array(doy_metadata).map(lambda d: ee.Number(d).int().format()) - spatialRed = getattr(ee.Reducer, spatialReducer)() # .setOutputs(doyList) + spatialRed = ( + getattr(ee.Reducer, spatialReducer)() + if isinstance(spatialReducer, str) + else spatialReducer + ) def spatialReduce(label: ee.String) -> ee.Dictionary: image = ic.select([label]).toBands().rename(doyList) @@ -1283,8 +1290,8 @@ def doyByRegions( band: str, regions: ee.FeatureCollection, label: str = "system:index", - spatialReducer: str = "mean", - timeReducer: str = "mean", + spatialReducer: str | ee.Reducer = "mean", + timeReducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", ) -> ee.Dictionary: @@ -1304,8 +1311,8 @@ def doyByRegions( band: The band to reduce. regions: The regions to reduce the data on. label: The property to use as label for each region. Default is "system:index". - spatialReducer: The name of the reducer to use. Default is "mean". - timeReducer: The name of the reducer to use for the temporal reduction. Default is "mean". + spatialReducer: The name of the reducer or a reducer object to use for spatial reduction. Default is "mean". + timeReducer: The name of the reducer or a reducer object to use for time reduction. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". @@ -1334,7 +1341,9 @@ def filter_doy(d: ee.Number) -> ee.ImageCollection: # reduce every sub ImageCollection in the list into images (it's the temporal reduction) # and aggregate the result as a single ImageCollection - timeRed = getattr(ee.Reducer, timeReducer)() # .setOutputs(band) + timeRed = ( + getattr(ee.Reducer, timeReducer)() if isinstance(timeReducer, str) else timeReducer + ) def timeReduce(c: ee.imageCollection) -> ee.image: c = ee.ImageCollection(c) @@ -1346,7 +1355,11 @@ def timeReduce(c: ee.imageCollection) -> ee.image: # reduce the data for each region doyList = ic.aggregate_array(doy_metadata).map(lambda d: ee.Number(d).int().format()) - spatialRed = getattr(ee.Reducer, spatialReducer)() # .setOutputs(doyList) + spatialRed = ( + getattr(ee.Reducer, spatialReducer)() + if isinstance(spatialReducer, str) + else spatialReducer + ) image = ic.toBands().rename(doyList) reduced = image.reduceRegions(regions, spatialRed, scale) @@ -1360,7 +1373,7 @@ def doyByYears( self, band: str, region: ee.Geometry, - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", ) -> ee.Dictionary: @@ -1379,7 +1392,7 @@ def doyByYears( Parameters: band: The band to reduce. region: The region to reduce the data on. - spatialReducer: The name of the reducer to use. Default is "mean". + reducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". @@ -1416,11 +1429,11 @@ def date_tag(i: ee.Image) -> ee.Image: # create a List of image collection where every images from the same year are grouped together yearList = ic.aggregate_array(year_metadata).distinct().sort() yearKeys = yearList.map(lambda y: ee.Number(y).int().format()) + red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer def reduce(year: ee.Number) -> ee.Dictionary: c = ic.filter(ee.Filter.eq(year_metadata, year)) doyList = c.aggregate_array(doy_metadata).map(lambda d: ee.Number(d).int().format()) - red = getattr(ee.Reducer, reducer)() # .setOutputs(doyList) return c.toBands().rename(doyList).reduceRegion(red, region, scale) return ee.Dictionary.fromLists(yearKeys, yearList.map(reduce)) @@ -1428,7 +1441,7 @@ def reduce(year: ee.Number) -> ee.Dictionary: def plot_dates_by_bands( self, region: ee.Geometry, - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", bands: list = [], @@ -1442,7 +1455,7 @@ def plot_dates_by_bands( Parameters: region: The region to reduce the data on. - reducer: The name of the reducer to use. Default is "mean". + reducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". bands: The bands to reduce. If empty, all bands are reduced. @@ -1488,7 +1501,7 @@ def plot_dates_by_regions( band: str, regions: ee.FeatureCollection, label: str = "system:index", - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", colors: list = [], @@ -1502,7 +1515,7 @@ def plot_dates_by_regions( band: The band to reduce. regions: The regions to reduce the data on. label: The property to use as label for each region. Default is "system:index". - reducer: The name of the reducer to use. Default is "mean". + reducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". colors: The colors to use for the regions. If empty, the default colors are used. @@ -1548,8 +1561,8 @@ def to_date(dict): def plot_doy_by_bands( self, region: ee.Geometry, - spatialReducer: str = "mean", - timeReducer: str = "mean", + spatialReducer: str | ee.Reducer = "mean", + timeReducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", bands: list = [], @@ -1563,8 +1576,8 @@ def plot_doy_by_bands( Parameters: region: The region to reduce the data on. - spatialReducer: The name of the reducer to use. Default is "mean". - timeReducer: The name of the reducer to use for the temporal reduction. Default is "mean". + spatialReducer: The name of the reducer or a reducer object to use. Default is "mean". + timeReducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". bands: The bands to reduce. If empty, all bands are reduced. @@ -1612,8 +1625,8 @@ def plot_doy_by_regions( band: str, regions: ee.FeatureCollection, label: str = "system:index", - spatialReducer: str = "mean", - timeReducer: str = "mean", + spatialReducer: str | ee.Reducer = "mean", + timeReducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", colors: list = [], @@ -1627,8 +1640,8 @@ def plot_doy_by_regions( band: The band to reduce. regions: The regions to reduce the data on. label: The property to use as label for each region. Default is "system:index". - spatialReducer: The name of the reducer to use. Default is "mean". - timeReducer: The name of the reducer to use for the temporal reduction. Default is "mean". + spatialReducer: The name of the reducer or a reducer object to use. Default is "mean". + timeReducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". colors: The colors to use for the regions. If empty, the default colors are used. @@ -1677,7 +1690,7 @@ def plot_doy_by_years( self, band: str, region: ee.Geometry, - reducer: str = "mean", + reducer: str | ee.Reducer = "mean", scale: int = 10000, dateProperty: str = "system:time_start", colors: list = [], @@ -1690,7 +1703,7 @@ def plot_doy_by_years( Parameters: band: The band to reduce. region: The region to reduce the data on. - reducer: The name of the reducer to use. Default is "mean". + reducer: The name of the reducer or a reducer object to use. Default is "mean". scale: The scale in meters to use for the reduction. default is 10000m dateProperty: The property to use as date for each image. Default is "system:time_start". colors: The colors to use for the regions. If empty, the default colors are used. diff --git a/tests/test_ImageCollection.py b/tests/test_ImageCollection.py index c099d542..c25ef042 100644 --- a/tests/test_ImageCollection.py +++ b/tests/test_ImageCollection.py @@ -329,14 +329,6 @@ def test_reduce_interval_with_non_existing_reducer_and_properties(self, jaxa_rai with pytest.raises(AttributeError): ic.geetools.reduceInterval("toto") - def test_reduce_interval_quality_mosaic(self, jaxa_rainfall, amazonas, num_regression): - # get 3 month worth of data and group it with default parameters - ic = jaxa_rainfall.filterDate("2020-01-01", "2020-03-31") - reduced = ic.geetools.reduceInterval("qualityMosaic", qualityBand="gaugeQualityInfo") - values = reduce(reduced, amazonas).getInfo() - values = {k: np.nan if v is None else v for k, v in values.items()} - num_regression.check(values) - def test_deprecated_reduce_equal_interval(self, jaxa_rainfall, amazonas, num_regression): # get 3 month worth of data and group it with default parameters ic = jaxa_rainfall.filterDate("2020-01-01", "2020-03-31") diff --git a/tests/test_ImageCollection/test_reduce_interval_quality_mosaic.csv b/tests/test_ImageCollection/test_reduce_interval_quality_mosaic.csv deleted file mode 100644 index 07a95cfd..00000000 --- a/tests/test_ImageCollection/test_reduce_interval_quality_mosaic.csv +++ /dev/null @@ -1,2 +0,0 @@ -,gaugeQualityInfo,hourlyPrecipRate,hourlyPrecipRateGC,observationTimeFlag,satelliteInfoFlag -0,0,0,8.6535346781602129e-06,-0.050000011920928955,-1