diff --git a/odc/stats/plugins/_registry.py b/odc/stats/plugins/_registry.py index 3cae2bd4..3f428d89 100644 --- a/odc/stats/plugins/_registry.py +++ b/odc/stats/plugins/_registry.py @@ -39,10 +39,11 @@ def import_all(): # TODO: make that more automatic modules = [ - "odc.stats.plugins.lc_treelite_cultivated.py", + "odc.stats.plugins.lc_treelite_cultivated", "odc.stats.plugins.lc_level3", "odc.stats.plugins.lc_treelite_woody", "odc.stats.plugins.lc_tf_urban", + "odc.stats.plugins.lc_level34", "odc.stats.plugins.lc_veg_class_a1", "odc.stats.plugins.lc_fc_wo_a0", "odc.stats.plugins.mangroves", diff --git a/odc/stats/plugins/l34_utils/ __init__.py b/odc/stats/plugins/l34_utils/ __init__.py new file mode 100644 index 00000000..e69de29b diff --git a/odc/stats/plugins/l34_utils/l4_bare_gradation.py b/odc/stats/plugins/l34_utils/l4_bare_gradation.py new file mode 100644 index 00000000..86e80173 --- /dev/null +++ b/odc/stats/plugins/l34_utils/l4_bare_gradation.py @@ -0,0 +1,46 @@ +import xarray as xr +from odc.stats._algebra import expr_eval + + +NODATA = 255 + + +def bare_gradation(xx: xr.Dataset, bare_threshold, veg_cover): + + # Map any data > 100 ---> 100 + bs_pc_50 = expr_eval( + "where((a>100)&(a!=nodata), 100, a)", + {"a": xx.bs_pc_50.data}, + name="mark_veg", + dtype="uint8", + **{"nodata": NODATA}, + ) + + # 60% <= data --> 15 + bs_mask = expr_eval( + "where((a>=m)&(a!=nodata), 15, a)", + {"a": bs_pc_50}, + name="mark_veg", + dtype="uint8", + **{"m": bare_threshold[1], "nodata": NODATA}, + ) + + # 20% <= data < 60% --> 12 + bs_mask = expr_eval( + "where((a>=m)&(a 10 + bs_mask = expr_eval( + "where(a 100 ---> 100 + pv_pc_50 = expr_eval( + "where((a>100) & (a!=nodata), 100, a)", + { + "a": pv_pc_50, + }, + name="mark_veg", + dtype="uint8", + **{"nodata": NODATA}, + ) + + # ## data < 1 ---> 0 + veg_mask = expr_eval( + "where(a 16 + veg_mask = expr_eval( + "where((a>=m)&(a 15 + veg_mask = expr_eval( + "where((a>=m)&(a 13 + veg_mask = expr_eval( + "where((a>=m)&(a 12 + veg_mask = expr_eval( + "where((a>=m)&(a 10 + veg_mask = expr_eval( + "where((a>=m)&(a 1 + water_mask = expr_eval( + "where((a>=m)&(a!=nodata), 1, a)", + {"a": water_frequency}, + name="mark_water", + dtype="uint8", + **{"m": watper_threshold[3], "nodata": NODATA}, + ) + + # 7 <= water_frequency < 10 --> 7 + water_mask = expr_eval( + "where((a>=m)&(a 8 + water_mask = expr_eval( + "where((a>=m)&(a 9 + water_mask = expr_eval( + "where((a>=m)&(a 1 woody + # 114 ----> 2 herbaceous + lifeform_mask = expr_eval( + "where(a==113, 1, a)", + {"a": xx.woody_cover.data}, + name="mark_lifeform", + dtype="uint8", + ) + lifeform_mask = expr_eval( + "where(a==114, 2, a)", + {"a": lifeform_mask}, + name="mark_lifeform", + dtype="uint8", + ) + + return lifeform_mask diff --git a/odc/stats/plugins/l34_utils/lc_water_seasonality.py b/odc/stats/plugins/l34_utils/lc_water_seasonality.py new file mode 100644 index 00000000..b727831d --- /dev/null +++ b/odc/stats/plugins/l34_utils/lc_water_seasonality.py @@ -0,0 +1,37 @@ +import xarray as xr + +from odc.stats._algebra import expr_eval + +WATER_FREQ_NODATA = -999 +NODATA = 255 + + +def water_seasonality(xx: xr.Dataset, water_seasonality_threshold): + # >= 3 months ----> 1 Semi-permanent or permanent + # < 3 months ----> 2 Temporary or seasonal + + # Apply nodata + water_frequency = expr_eval( + "where((a==watersea_nodata), nodata, a)", + {"a": xx.water_frequency.data}, + name="mark_water_season", + dtype="uint8", + **{"watersea_nodata": WATER_FREQ_NODATA, "nodata": NODATA}, + ) + + water_season_mask = expr_eval( + "where((a>watseas_trh)&(a<=12), 1, a)", + {"a": water_frequency}, + name="mark_water_season", + dtype="uint8", + **{"watseas_trh": water_seasonality_threshold, "nodata": NODATA}, + ) + water_season_mask = expr_eval( + "where((a<=watseas_trh)&(a<=12), 2, b)", + {"a": water_frequency, "b": water_season_mask}, + name="mark_water_season", + dtype="uint8", + **{"watseas_trh": water_seasonality_threshold}, + ) + + return water_season_mask diff --git a/odc/stats/plugins/l34_utils/utils.py b/odc/stats/plugins/l34_utils/utils.py new file mode 100644 index 00000000..3b0444eb --- /dev/null +++ b/odc/stats/plugins/l34_utils/utils.py @@ -0,0 +1,10 @@ +import xarray as xr + + +def apply_mapping(data, class_mapping): + """ + Utility function to apply mapping on dictionaries + """ + for o_val, n_val in class_mapping.items(): + data = xr.where(data == o_val, n_val, data) + return data diff --git a/odc/stats/plugins/lc_level3.py b/odc/stats/plugins/lc_level3.py deleted file mode 100644 index 2d062c41..00000000 --- a/odc/stats/plugins/lc_level3.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Land Cover Level3 classification -""" - -from typing import Tuple -import xarray as xr -from odc.stats._algebra import expr_eval -from ._registry import StatsPluginInterface, register - -NODATA = 255 - - -class StatsLccsLevel3(StatsPluginInterface): - NAME = "ga_ls_lccs_level3" - SHORT_NAME = NAME - VERSION = "0.0.1" - PRODUCT_FAMILY = "lccs" - - @property - def measurements(self) -> Tuple[str, ...]: - _measurements = ["level3_class"] - return _measurements - - def fuser(self, xx: xr.Dataset) -> xr.Dataset: - return xx - - def reduce(self, xx: xr.Dataset) -> xr.Dataset: - - # Cultivated pipeline applies a mask which feeds only terrestrial veg (110) to the model - # Just exclude no data (255 or nan) and apply the cultivated results - # 255: load with product definition; nan: load without - # hence accormmodate both - - res = expr_eval( - "where((a!=a)|(a>=nodata), b, a)", - {"a": xx.cultivated_class.data, "b": xx.classes_l3_l4.data}, - name="mask_cultivated", - dtype="float32", - **{"nodata": xx.cultivated_class.attrs.get("nodata")}, - ) - - # Mask urban results with bare sfc (210) - - res = expr_eval( - "where(a==_u, b, a)", - { - "a": res, - "b": xx.urban_classes.data, - }, - name="mark_urban", - dtype="float32", - **{"_u": 210}, - ) - - # Mark nodata to 255 in case any nan - - res = expr_eval( - "where(a==a, a, nodata)", - { - "a": res, - }, - name="mark_nodata", - dtype="uint8", - **{"nodata": NODATA}, - ) - - attrs = xx.attrs.copy() - attrs["nodata"] = NODATA - dims = xx.classes_l3_l4.dims[1:] - - data_vars = { - "level3_class": xr.DataArray(res.squeeze(), dims=dims, attrs=attrs) - } - - coords = dict((dim, xx.coords[dim]) for dim in dims) - level3 = xr.Dataset(data_vars=data_vars, coords=coords, attrs=attrs) - - return level3 - - -register("lccs_level3", StatsLccsLevel3) diff --git a/odc/stats/plugins/lc_level34.py b/odc/stats/plugins/lc_level34.py new file mode 100644 index 00000000..bafd6f61 --- /dev/null +++ b/odc/stats/plugins/lc_level34.py @@ -0,0 +1,128 @@ +""" +Plugin of Module A3 in LandCover PipeLine +""" + +from typing import Tuple, Optional, List + +import numpy as np +import xarray as xr + +from ._registry import StatsPluginInterface, register + +from .l34_utils import ( + l4_water_persistence, + lc_water_seasonality, + l4_veg_cover, + lc_level3, + l4_cultivated, + l4_natural_veg, + l4_natural_aquatic, + l4_surface, + l4_bare_gradation, + l4_water, + lc_lifeform, + lc_intertidal_mask, +) + + +NODATA = 255 + + +class StatsLccsLevel4(StatsPluginInterface): + NAME = "ga_ls_lccs_Level34" + SHORT_NAME = NAME + VERSION = "0.0.1" + PRODUCT_FAMILY = "lccs" + + def __init__( + self, + veg_threshold: Optional[List] = None, + bare_threshold: Optional[List] = None, + watper_threshold: Optional[List] = None, + water_seasonality_threshold: int = None, + **kwargs, + ): + super().__init__(**kwargs) + + self.veg_threshold = ( + veg_threshold if veg_threshold is not None else [1, 4, 15, 40, 65, 100] + ) + self.bare_threshold = bare_threshold if bare_threshold is not None else [20, 60] + self.watper_threshold = ( + watper_threshold if watper_threshold is not None else [1, 4, 7, 10] + ) + self.water_seasonality_threshold = ( + water_seasonality_threshold if water_seasonality_threshold else 3 + ) + + @property + def measurements(self) -> Tuple[str, ...]: + _measurements = ["level3", "level4"] + return _measurements + + def native_transform(self, xx): + return xx + + def fuser(self, xx): + return xx + + def reduce(self, xx: xr.Dataset) -> xr.Dataset: + + # Water persistence + water_persistence = l4_water_persistence.water_persistence( + xx, self.watper_threshold + ) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, self.water_seasonality_threshold + ) + + intertidal_mask = lc_intertidal_mask.intertidal_mask(xx) + + # #TODO WATER (99-104) + l4 = l4_water.water_classification(xx, intertidal_mask, water_persistence) + + # Generate Level3 classes + level3 = lc_level3.lc_level3(xx) + + # Vegetation cover + veg_cover = l4_veg_cover.canopyco_veg_con(xx, self.veg_threshold) + + # Define life form + lifeform = lc_lifeform.lifeform(xx) + + # Apply cultivated Level-4 classes (1-18) + l4 = l4_cultivated.lc_l4_cultivated(l4, level3, lifeform, veg_cover) + + # Apply terrestrial vegetation classes [19-36] + l4 = l4_natural_veg.lc_l4_natural_veg(l4, level3, lifeform, veg_cover) + + # Bare gradation + bare_gradation = l4_bare_gradation.bare_gradation( + xx, self.bare_threshold, veg_cover + ) + + l4 = l4_natural_aquatic.natural_auquatic_veg( + l4, lifeform, veg_cover, water_seasonality + ) + + level4 = l4_surface.lc_l4_surface(l4, level3, bare_gradation) + + level3 = level3.astype(np.uint8) + level4 = level4.astype(np.uint8) + + attrs = xx.attrs.copy() + attrs["nodata"] = NODATA + dims = xx.classes_l3_l4.dims[1:] + + data_vars = { + "level3": xr.DataArray(level3.squeeze(), dims=dims, attrs=attrs), + "level4": xr.DataArray(level4.squeeze(), dims=dims, attrs=attrs), + } + + coords = dict((dim, xx.coords[dim]) for dim in dims) + leve34 = xr.Dataset(data_vars=data_vars, coords=coords, attrs=xx.attrs) + return leve34 + + +register("lc_l3_l4", StatsLccsLevel4) diff --git a/tests/test_landcover_plugin_a0.py b/tests/test_landcover_plugin_a0.py index 3d6463f0..5a2369a3 100644 --- a/tests/test_landcover_plugin_a0.py +++ b/tests/test_landcover_plugin_a0.py @@ -1,3 +1,7 @@ +""" +Unit tests for Cultivate Terrestrial Vegetation +""" + from functools import partial import numpy as np import xarray as xr diff --git a/tests/test_lc_l34.py b/tests/test_lc_l34.py new file mode 100644 index 00000000..397fe761 --- /dev/null +++ b/tests/test_lc_l34.py @@ -0,0 +1,159 @@ +from odc.stats.plugins.lc_level34 import StatsLccsLevel4 +import numpy as np +import pandas as pd +import xarray as xr +import dask.array as da + +import pytest + + +NODATA = 255 + + +@pytest.fixture(scope="module") +def image_groups(): + l34 = np.array( + [ + [ + [210, 210, 210], + [210, 210, 210], + [210, 210, 210], + [210, 210, 210], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 215], + [216, 216, 216], + [215, 215, 215], + [215, 215, 215], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [113, 113, 255], + [114, 114, 114], + [114, 114, 255], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 61, 78], + [4, 23, 42], + ] + ], + dtype="uint8", + ) + + bs_pc_50 = np.array( + [ + [ + [1, 64, NODATA], + [66, 40, 41], + [1, 40, 66], + [NODATA, 1, 42], + ] + ], + dtype="uint8", + ) + + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + water_frequency = np.array( + [ + [ + [1, 3, 2], + [4, 5, 6], + [9, 2, 11], + [10, 11, 12], + ] + ], + dtype="uint8", + ) + + tuples = [ + (np.datetime64("2000-01-01T00"), np.datetime64("2000-01-01")), + ] + index = pd.MultiIndex.from_tuples(tuples, names=["time", "solar_day"]) + coords = { + "x": np.linspace(10, 20, l34.shape[2]), + "y": np.linspace(0, 5, l34.shape[1]), + "spec": index, + } + + data_vars = { + "classes_l3_l4": xr.DataArray( + da.from_array(l34, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "urban_classes": xr.DataArray( + da.from_array(urban, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "cultivated_class": xr.DataArray( + da.from_array(cultivated, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "woody_cover": xr.DataArray( + da.from_array(woody, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "pv_pc_50": xr.DataArray( + da.from_array(pv_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "bs_pc_50": xr.DataArray( + da.from_array(bs_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "water_frequency": xr.DataArray( + da.from_array(water_frequency, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + } + + xx = xr.Dataset(data_vars=data_vars, coords=coords) + return xx + + +def test_l4_classes(image_groups): + expected_l3 = [[216, 216, 215], [216, 216, 216], [215, 215, 215], [215, 215, 215]] + + expected_l4 = [[95, 97, 93], [97, 96, 96], [93, 93, 93], [93, 93, 93]] + stats_l4 = StatsLccsLevel4() + ds = stats_l4.reduce(image_groups) + + assert (ds.level3.compute() == expected_l3).all() + assert (ds.level4.compute() == expected_l4).all() diff --git a/tests/test_lc_l4_ctv.py b/tests/test_lc_l4_ctv.py new file mode 100644 index 00000000..a8d89745 --- /dev/null +++ b/tests/test_lc_l4_ctv.py @@ -0,0 +1,381 @@ +import numpy as np +import xarray as xr +import dask.array as da + +from odc.stats.plugins.lc_level34 import StatsLccsLevel4 +from odc.stats.plugins.l34_utils import ( + l4_cultivated, + lc_level3, + l4_veg_cover, + lc_lifeform, +) + +import pandas as pd + +NODATA = 255 + + +def image_groups(l34, urban, cultivated, woody, pv_pc_50): + + tuples = [ + (np.datetime64("2000-01-01T00"), np.datetime64("2000-01-01")), + ] + index = pd.MultiIndex.from_tuples(tuples, names=["time", "solar_day"]) + coords = { + "x": np.linspace(10, 20, l34.shape[2]), + "y": np.linspace(0, 5, l34.shape[1]), + "spec": index, + } + + data_vars = { + "classes_l3_l4": xr.DataArray( + da.from_array(l34, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "urban_classes": xr.DataArray( + da.from_array(urban, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "cultivated_class": xr.DataArray( + da.from_array(cultivated, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + ), + "woody_cover": xr.DataArray( + da.from_array(woody, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "pv_pc_50": xr.DataArray( + da.from_array(pv_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + } + xx = xr.Dataset(data_vars=data_vars, coords=coords) + return xx + + +def test_ctv_classes_woody(): + + expected_cultivated_classes = [ + [13, 10, 9], + [110, 10, 10], + [13, 11, 11], + [12, 13, 10], + ] + + l34 = np.array( + [ + [ + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 111 --> cultivated + cultivated = np.array( + [ + [ + [111, 111, 111], + [255, 111, 111], + [111, 111, 111], + [111, 111, 111], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + assert (l4_ctv.compute() == expected_cultivated_classes).all() + + +def test_ctv_classes_herbaceous(): + + expected_cultivated_classes = [ + [18, 15, 14], + [110, 15, 15], + [18, 16, 16], + [17, 18, 15], + ] + + l34 = np.array( + [ + [ + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + + cultivated = np.array( + [ + [ + [111, 111, 111], + [255, 111, 111], + [111, 111, 111], + [111, 111, 111], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + assert (l4_ctv.compute() == expected_cultivated_classes).all() + + +def test_ctv_classes_woody_herbaceous(): + + expected_cultivated_classes = [ + [13, 10, 9], + [110, 15, 15], + [13, 11, 11], + [17, 18, 15], + ] + + l34 = np.array( + [ + [ + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + ] + ], + dtype="int", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="int", + ) + + cultivated = np.array( + [ + [ + [111, 111, 111], + [255, 111, 111], + [111, 111, 111], + [111, 111, 111], + ] + ], + dtype="int", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [114, 114, 114], + [113, 113, 113], + [114, 114, 114], + ] + ], + dtype="int", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="int", + ) + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + assert (l4_ctv.compute() == expected_cultivated_classes).all() + + +def test_ctv_classes_no_vegcover(): + + expected_cultivated_classes = [ + [2, 2, 2], + [110, 3, 3], + [2, 2, 2], + [3, 3, 3], + ] + + l34 = np.array( + [ + [ + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + ] + ], + dtype="int", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="int", + ) + + cultivated = np.array( + [ + [ + [111, 111, 111], + [255, 111, 111], + [111, 111, 111], + [111, 111, 111], + ] + ], + dtype="int", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [114, 114, 114], + [113, 113, 113], + [114, 114, 114], + ] + ], + dtype="int", + ) + + pv_pc_50 = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="int", + ) + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + assert (l4_ctv.compute() == expected_cultivated_classes).all() diff --git a/tests/test_lc_l4_natural_surface.py b/tests/test_lc_l4_natural_surface.py new file mode 100644 index 00000000..131c95c9 --- /dev/null +++ b/tests/test_lc_l4_natural_surface.py @@ -0,0 +1,203 @@ +""" + Unit tests for LandCover Natural Aquatic Vegetation classes +""" + +import numpy as np +import xarray as xr +import dask.array as da + +from odc.stats.plugins.lc_level34 import StatsLccsLevel4 +from odc.stats.plugins.l34_utils import ( + l4_cultivated, + lc_level3, + l4_veg_cover, + l4_natural_veg, + l4_natural_aquatic, + l4_surface, + l4_bare_gradation, + lc_lifeform, + lc_water_seasonality, +) + +import pandas as pd + +NODATA = 255 + + +def image_groups(l34, urban, woody, bs_pc_50, pv_pc_50, cultivated, water_frequency): + + tuples = [ + (np.datetime64("2000-01-01T00"), np.datetime64("2000-01-01")), + ] + index = pd.MultiIndex.from_tuples(tuples, names=["time", "solar_day"]) + coords = { + "x": np.linspace(10, 20, l34.shape[2]), + "y": np.linspace(0, 5, l34.shape[1]), + "spec": index, + } + + data_vars = { + "classes_l3_l4": xr.DataArray( + da.from_array(l34, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "urban_classes": xr.DataArray( + da.from_array(urban, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "cultivated_class": xr.DataArray( + da.from_array(cultivated, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "woody_cover": xr.DataArray( + da.from_array(woody, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "pv_pc_50": xr.DataArray( + da.from_array(pv_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "bs_pc_50": xr.DataArray( + da.from_array(bs_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "water_frequency": xr.DataArray( + da.from_array(water_frequency, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + } + xx = xr.Dataset(data_vars=data_vars, coords=coords) + return xx + + +def test_ns(): + expected_l4_srf_classes = [ + [95, 97, 93], + [97, 96, 96], + [95, 95, 95], + [94, 95, 96], + ] + + l34 = np.array( + [ + [ + [210, 210, 210], + [210, 210, 210], + [210, 210, 210], + [210, 210, 210], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 215], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [113, 113, 255], + [114, 114, 114], + [114, 114, 255], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + + bs_pc_50 = np.array( + [ + [ + [1, 64, NODATA], + [66, 40, 41], + [3, 16, 15], + [NODATA, 1, 42], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + water_frequency = np.array( + [ + [ + [1, 3, 2], + [4, 5, 6], + [9, 2, 11], + [10, 11, 12], + ] + ], + dtype="uint8", + ) + + xx = image_groups( + l34, urban, woody, bs_pc_50, pv_pc_50, cultivated, water_frequency + ) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + + # Bare gradation + bare_gradation = l4_bare_gradation.bare_gradation( + xx, stats_l4.bare_threshold, veg_cover + ) + + l4_ctv_ntv_nav_surface = l4_surface.lc_l4_surface( + l4_ctv_ntv_nav, level3, bare_gradation + ) + + assert (l4_ctv_ntv_nav_surface.compute() == expected_l4_srf_classes).all() diff --git a/tests/test_lc_l4_nav.py b/tests/test_lc_l4_nav.py new file mode 100644 index 00000000..6cd05126 --- /dev/null +++ b/tests/test_lc_l4_nav.py @@ -0,0 +1,872 @@ +""" + Unit tests for LandCover Natural Aquatic Vegetation classes +""" + +import numpy as np +import xarray as xr +import dask.array as da + +from odc.stats.plugins.lc_level34 import StatsLccsLevel4 +from odc.stats.plugins.l34_utils import ( + l4_cultivated, + lc_level3, + l4_veg_cover, + l4_natural_veg, + l4_natural_aquatic, + lc_lifeform, + lc_water_seasonality, +) + +import pandas as pd + +NODATA = 255 + + +def image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency): + + tuples = [ + (np.datetime64("2000-01-01T00"), np.datetime64("2000-01-01")), + ] + index = pd.MultiIndex.from_tuples(tuples, names=["time", "solar_day"]) + coords = { + "x": np.linspace(10, 20, l34.shape[2]), + "y": np.linspace(0, 5, l34.shape[1]), + "spec": index, + } + + data_vars = { + "classes_l3_l4": xr.DataArray( + da.from_array(l34, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "urban_classes": xr.DataArray( + da.from_array(urban, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "cultivated_class": xr.DataArray( + da.from_array(cultivated, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "woody_cover": xr.DataArray( + da.from_array(woody, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "pv_pc_50": xr.DataArray( + da.from_array(pv_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "water_frequency": xr.DataArray( + da.from_array(water_frequency, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + } + xx = xr.Dataset(data_vars=data_vars, coords=coords) + return xx + + +def test_ntv_classes_woody_herbaceous(): + expected_l4_ntv_classes = [[56, 56, 56], [56, 56, 55], [57, 57, 57], [57, 57, 55]] + + l34 = np.array( + [ + [ + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [113, 113, 255], + [114, 114, 114], + [114, 114, 255], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + water_frequency = np.array( + [ + [ + [1, 3, 2], + [4, 5, 6], + [9, 2, 11], + [10, 11, 12], + ] + ], + dtype="uint8", + ) + + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + + assert (l4_ctv_ntv_nav.compute() == expected_l4_ntv_classes).all() + + +def test_ntv_veg_cover(): + expected_l4_ntv_classes = [ + [62, 59, 58], + [58, 59, 59], + [62, 60, 60], + [61, 62, 59], + ] + + l34 = np.array( + [ + [ + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [-999, -999, -999], + [-999, -999, -999], + [-999, -999, -999], + [-999, -999, -999], + ] + ], + dtype="uint8", + ) + + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + assert (l4_ctv_ntv_nav.compute() == expected_l4_ntv_classes).all() + + +def test_ntv_woody_veg_cover(): + expected_l4_ntv_classes = [ + [75, 66, 63], + [63, 66, 66], + [75, 69, 69], + [72, 75, 66], + ] + + l34 = np.array( + [ + [ + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [-9999, -9999, -9999], + [-9999, -9999, -9999], + [-9999, -9999, -9999], + [-9999, -9999, -9999], + ] + ], + dtype="uint8", + ) + + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + assert (l4_ctv_ntv_nav.compute() == expected_l4_ntv_classes).all() + + +def test_ntv_woody_seasonal_water_veg_cover(): + expected_l4_ntv_classes = [ + [77, 68, 65], + [65, 68, 68], + [77, 71, 71], + [74, 77, 68], + ] + + l34 = np.array( + [ + [ + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + ] + ], + dtype="uint8", + ) + + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + + assert (l4_ctv_ntv_nav.compute() == expected_l4_ntv_classes).all() + + +def test_ntv_woody_permanent_water_veg_cover(): + expected_l4_ntv_classes = [ + [76, 67, 64], + [64, 67, 67], + [76, 70, 70], + [73, 76, 67], + ] + + l34 = np.array( + [ + [ + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + [11, 10, 8], + ] + ], + dtype="uint8", + ) + + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + assert (l4_ctv_ntv_nav.compute() == expected_l4_ntv_classes).all() + + +def test_ntv_herbaceous_veg_cover(): + expected_l4_ntv_classes = [[90, 81, 78], [78, 81, 81], [90, 84, 84], [87, 90, 81]] + + l34 = np.array( + [ + [ + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [-9999, -9999, -9999], + [-9999, -9999, -9999], + [-9999, -9999, -9999], + [-9999, -9999, -9999], + ] + ], + dtype="uint8", + ) + + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + assert (l4_ctv_ntv_nav.compute() == expected_l4_ntv_classes).all() + + +def test_ntv_herbaceous_seasonal_water_veg_cover(): + expected_l4_ntv_classes = [ + [92, 83, 80], + [80, 83, 83], + [92, 86, 86], + [89, 92, 83], + ] + + l34 = np.array( + [ + [ + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + [1, 2, 3], + ] + ], + dtype="uint8", + ) + + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + + assert (l4_ctv_ntv_nav.compute() == expected_l4_ntv_classes).all() + + +def test_ntv_herbaceous_permanent_water_veg_cover(): + expected_l4_ntv_classes = [ + [91, 82, 79], + [79, 82, 82], + [91, 85, 85], + [88, 91, 82], + ] + + l34 = np.array( + [ + [ + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + [124, 124, 124], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + [11, 10, 8], + ] + ], + dtype="uint8", + ) + + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50, water_frequency) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + # Apply cultivated to match the code in Level4 processing + l4_ctv = l4_cultivated.lc_l4_cultivated( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + l4_ctv_ntv = l4_natural_veg.lc_l4_natural_veg(l4_ctv, level3, lifeform, veg_cover) + + water_seasonality = lc_water_seasonality.water_seasonality( + xx, stats_l4.water_seasonality_threshold + ) + l4_ctv_ntv_nav = l4_natural_aquatic.natural_auquatic_veg( + l4_ctv_ntv, lifeform, veg_cover, water_seasonality + ) + assert (l4_ctv_ntv_nav.compute() == expected_l4_ntv_classes).all() diff --git a/tests/test_lc_l4_ntv.py b/tests/test_lc_l4_ntv.py new file mode 100644 index 00000000..d6e9bb08 --- /dev/null +++ b/tests/test_lc_l4_ntv.py @@ -0,0 +1,383 @@ +""" + Unit tests for LandCover Natural Terrestrial Vegetated classes +""" + +import numpy as np +import xarray as xr +import dask.array as da + +from odc.stats.plugins.lc_level34 import StatsLccsLevel4 +from odc.stats.plugins.l34_utils import ( + lc_level3, + l4_veg_cover, + l4_natural_veg, + lc_lifeform, +) + +import pandas as pd + +NODATA = 255 + + +def image_groups(l34, urban, cultivated, woody, pv_pc_50): + + tuples = [ + (np.datetime64("2000-01-01T00"), np.datetime64("2000-01-01")), + ] + index = pd.MultiIndex.from_tuples(tuples, names=["time", "solar_day"]) + coords = { + "x": np.linspace(10, 20, l34.shape[2]), + "y": np.linspace(0, 5, l34.shape[1]), + "spec": index, + } + + data_vars = { + "classes_l3_l4": xr.DataArray( + da.from_array(l34, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "urban_classes": xr.DataArray( + da.from_array(urban, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "cultivated_class": xr.DataArray( + da.from_array(cultivated, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "woody_cover": xr.DataArray( + da.from_array(woody, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "pv_pc_50": xr.DataArray( + da.from_array(pv_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + } + xx = xr.Dataset(data_vars=data_vars, coords=coords) + return xx + + +def test_ntv_classes_herbaceous(): + + expected_natural_terrestrial_veg_classes = [ + [36, 33, 32], + [110, 33, 33], + [36, 34, 34], + [35, 36, 33], + ] + + l34 = np.array( + [ + [ + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [112, 112, 112], + [255, 112, 112], + [112, 112, 112], + [112, 112, 112], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + l4_ntv = l4_natural_veg.lc_l4_natural_veg( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + assert (l4_ntv.compute() == expected_natural_terrestrial_veg_classes).all() + + +def test_ntv_classes_woody(): + + expected_natural_terrestrial_veg_classes = [ + [31, 28, 27], + [110, 28, 28], + [31, 29, 29], + [30, 26, 28], + ] + + l34 = np.array( + [ + [ + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [112, 112, 112], + [255, 112, 112], + [112, 112, 112], + [112, 112, 112], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [113, 113, 113], + [113, 113, 113], + [113, 255, 113], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + + l4_ntv = l4_natural_veg.lc_l4_natural_veg( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + assert (l4_ntv.compute() == expected_natural_terrestrial_veg_classes).all() + + +def test_ntv_classes_no_veg(): + + expected_natural_terrestrial_veg_classes = [ + [20, 20, 20], + [110, 21, 21], + [20, 20, 20], + [21, 21, 21], + ] + + l34 = np.array( + [ + [ + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [112, 112, 112], + [255, 112, 112], + [112, 112, 112], + [112, 112, 112], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [113, 113, 113], + [114, 114, 114], + [113, 113, 113], + [114, 114, 114], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + l4_ntv = l4_natural_veg.lc_l4_natural_veg( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + assert (l4_ntv.compute() == expected_natural_terrestrial_veg_classes).all() + + +def test_ntv_classes_no_lifeform(): + + expected_natural_terrestrial_veg_classes = [ + [26, 23, 22], + [22, 23, 23], + [26, 24, 24], + [25, 26, 23], + ] + + l34 = np.array( + [ + [ + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + [110, 110, 110], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [112, 112, 112], + [112, 112, 112], + [112, 112, 112], + [112, 112, 112], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + xx = image_groups(l34, urban, cultivated, woody, pv_pc_50) + + stats_l4 = StatsLccsLevel4() + level3 = lc_level3.lc_level3(xx) + lifeform = lc_lifeform.lifeform(xx) + veg_cover = l4_veg_cover.canopyco_veg_con(xx, stats_l4.veg_threshold) + l4_ntv = l4_natural_veg.lc_l4_natural_veg( + xx.classes_l3_l4, level3, lifeform, veg_cover + ) + assert (l4_ntv.compute() == expected_natural_terrestrial_veg_classes).all() diff --git a/tests/test_lc_l4_water.py b/tests/test_lc_l4_water.py new file mode 100644 index 00000000..df6596cb --- /dev/null +++ b/tests/test_lc_l4_water.py @@ -0,0 +1,290 @@ +""" + Unit tests for LandCover water classes +""" + +import numpy as np +import xarray as xr +import dask.array as da + +from odc.stats.plugins.lc_level34 import StatsLccsLevel4 +from odc.stats.plugins.l34_utils import ( + l4_water_persistence, + l4_water, + lc_intertidal_mask, +) + +import pandas as pd + +NODATA = 255 +WATER_FREQ_NODATA = -999 + + +# @pytest.fixture(scope="module") +def image_groups(l34, urban, cultivated, woody, bs_pc_50, pv_pc_50, water_frequency): + + tuples = [ + (np.datetime64("2000-01-01T00"), np.datetime64("2000-01-01")), + ] + index = pd.MultiIndex.from_tuples(tuples, names=["time", "solar_day"]) + coords = { + "x": np.linspace(10, 20, l34.shape[2]), + "y": np.linspace(0, 5, l34.shape[1]), + "spec": index, + } + + data_vars = { + "classes_l3_l4": xr.DataArray( + da.from_array(l34, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "urban_classes": xr.DataArray( + da.from_array(urban, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "cultivated_class": xr.DataArray( + da.from_array(cultivated, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "woody_cover": xr.DataArray( + da.from_array(woody, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "pv_pc_50": xr.DataArray( + da.from_array(pv_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "bs_pc_50": xr.DataArray( + da.from_array(bs_pc_50, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + "water_frequency": xr.DataArray( + da.from_array(water_frequency, chunks=(1, -1, -1)), + dims=("spec", "y", "x"), + attrs={"nodata": 255}, + ), + } + xx = xr.Dataset(data_vars=data_vars, coords=coords) + return xx + + +def test_water_classes(): + expected_water_classes = [ + [104, 104, 104], + [103, 103, 103], + [102, 102, 101], + [98, 101, 101], + ] + + l34 = np.array( + [ + [ + [221, 221, 221], + [221, 221, 221], + [221, 221, 221], + [221, 221, 221], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [112, 112, 112], + [255, 112, 112], + [112, 112, 112], + [112, 112, 112], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + bs_pc_50 = np.array( + [ + [ + [1, 64, NODATA], + [66, 40, 41], + [3, 16, 15], + [NODATA, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [1, 3, 2], + [4, 5, 6], + [9, 7, 11], + [WATER_FREQ_NODATA, 11, 12], + ] + ], + dtype="float", + ) + xx = image_groups( + l34, urban, cultivated, woody, bs_pc_50, pv_pc_50, water_frequency + ) + + stats_l4 = StatsLccsLevel4() + intertidal_mask = lc_intertidal_mask.intertidal_mask(xx) + + # Water persistence + water_persistence = l4_water_persistence.water_persistence( + xx, stats_l4.watper_threshold + ) + + l4_water_classes = l4_water.water_classification( + xx, intertidal_mask, water_persistence + ) + + assert (l4_water_classes.compute() == expected_water_classes).all() + + +def test_water_intertidal(): + + expected_water_classes = [ + [104, 104, 104], + [103, 103, 103], + [102, 102, 101], + [101, 98, 100], + ] + + l34 = np.array( + [ + [ + [221, 221, 221], + [221, 221, 221], + [221, 221, 221], + [221, 221, 223], + ] + ], + dtype="uint8", + ) + + urban = np.array( + [ + [ + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + [216, 216, 216], + ] + ], + dtype="uint8", + ) + # 112 --> natural veg + cultivated = np.array( + [ + [ + [112, 112, 112], + [255, 112, 112], + [112, 112, 112], + [112, 112, 112], + ] + ], + dtype="uint8", + ) + + woody = np.array( + [ + [ + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + [114, 114, 114], + ] + ], + dtype="uint8", + ) + + pv_pc_50 = np.array( + [ + [ + [1, 64, 65], + [66, 40, 41], + [3, 16, 15], + [4, 1, 42], + ] + ], + dtype="uint8", + ) + bs_pc_50 = np.array( + [ + [ + [1, 64, NODATA], + [66, 40, 41], + [3, 16, 15], + [NODATA, 1, 42], + ] + ], + dtype="uint8", + ) + water_frequency = np.array( + [ + [ + [1, 3, 2], + [4, 5, 6], + [9, 7, 11], + [10, 255, 255], + ] + ], + dtype="uint8", + ) + xx = image_groups( + l34, urban, cultivated, woody, bs_pc_50, pv_pc_50, water_frequency + ) + + stats_l4 = StatsLccsLevel4() + intertidal_mask = lc_intertidal_mask.intertidal_mask(xx) + + # Water persistence + water_persistence = l4_water_persistence.water_persistence( + xx, stats_l4.watper_threshold + ) + + l4_water_classes = l4_water.water_classification( + xx, intertidal_mask, water_persistence + ) + print(l4_water_classes.compute()) + assert (l4_water_classes.compute() == expected_water_classes).all() diff --git a/tests/test_lc_level3.py b/tests/test_lc_level3.py index 524d4912..42170784 100644 --- a/tests/test_lc_level3.py +++ b/tests/test_lc_level3.py @@ -3,14 +3,16 @@ import xarray as xr import dask.array as da -from odc.stats.plugins.lc_level3 import StatsLccsLevel3 +from odc.stats.plugins.l34_utils import lc_level3 import pytest +NODATA = 255 + expected_l3_classes = [ [111, 112, 215], [124, 112, 215], - [221, 215, 216], - [223, 255, 223], + [220, 215, 216], + [220, 255, 220], ] @@ -83,8 +85,7 @@ def image_groups(): return xx -def test_urban_class(image_groups): +def test_l3_classes(image_groups): - lc_level3 = StatsLccsLevel3() - level3_classes = lc_level3.reduce(image_groups) - assert (level3_classes.level3_class.values == expected_l3_classes).all() + level3_classes = lc_level3.lc_level3(image_groups) + assert (level3_classes == expected_l3_classes).all()