diff --git a/doc/source/stats.md b/doc/source/stats.md index f640a788..55a9a193 100644 --- a/doc/source/stats.md +++ b/doc/source/stats.md @@ -65,11 +65,17 @@ rast = gu.Raster(filename_rast) rast ``` -Get all default statistics: +By default and without any specification, this function computes the following main statistics: +minimum, maximum, mean, standard deviation, NMAD, total count, and percentage of valid points. ```{code-cell} ipython3 rast.get_stats() ``` +To compute all available statistics, set `stats_name` to `all`. +```{code-cell} ipython3 +rast.get_stats("all") +``` + Get a single statistic (e.g., 'mean') as a float: ```{code-cell} ipython3 rast.get_stats("mean") diff --git a/geoutils/pointcloud/pointcloud.py b/geoutils/pointcloud/pointcloud.py index 035e9769..e4ebd7c7 100644 --- a/geoutils/pointcloud/pointcloud.py +++ b/geoutils/pointcloud/pointcloud.py @@ -19,7 +19,6 @@ from __future__ import annotations -import logging import os.path import pathlib import warnings @@ -1424,12 +1423,17 @@ def get_stats( Callable functions are supported as well. - :param stats_name: Name or list of names of the statistics to retrieve. If None, all statistics are returned. + By default and without any specification, this function computes the following main statistics: minimum, + maximum, mean, standard deviation, NMAD, total count, and percentage of valid points. + To compute all available statistics, set `stats_name` to `all`. + + :param stats_name: Name or list of names of the statistics to retrieve. If None, main statistics are returned. Accepted names include: `mean`, `median`, `max`, `min`, `sum`, `sum of squares`, `90th percentile`, `LE90`, `nmad`, `rmse`, `std`, `valid count`, `total count`, `percentage valid points` and if an inlier mask is passed : `valid inlier count`, `total inlier count`, `percentage inlier point`, `percentage valid inlier points`. Custom callables can also be provided. + To compute all available statistics, set `stats_name` to `all`. :returns: The requested statistic or a dictionary of statistics if multiple or all are requested. """ @@ -1440,7 +1444,7 @@ def get_stats( data = self.data # Given list or all attributes to compute if None - if isinstance(stats_name, list) or stats_name is None: + if isinstance(stats_name, list) or stats_name is None or stats_name == "all": return _statistics(data, stats_name) # type: ignore else: # Single attribute to compute @@ -1449,7 +1453,7 @@ def get_stats( elif callable(stats_name): return stats_name(data) # type: ignore else: - logging.warning("Statistic name '%s' is a not recognized string", stats_name) + warnings.warn("Statistic name " + str(stats_name) + " is a not recognized string", category=UserWarning) @overload def subsample( diff --git a/geoutils/raster/base.py b/geoutils/raster/base.py index 07325d8e..ac521a2e 100644 --- a/geoutils/raster/base.py +++ b/geoutils/raster/base.py @@ -727,7 +727,7 @@ def get_stats( - Std (Standard deviation): measures the spread or dispersion of the data around the mean, \ ignoring masked values. - Valid count: number of finite data points in the array. It counts the non-masked elements. - - Total count: total size of the raster. + - Total count: total size (width x height) of the raster. - Percentage valid points: ratio between Valid count and Total count. For all statistics up to and including "Std", NumPy Masked functions are used (directly or in the calculation) @@ -749,12 +749,17 @@ def get_stats( Callable functions are supported as well. - :param stats_name: Name or list of names of the statistics to retrieve. If None, all statistics are returned. + By default and without any specification, this function computes the following main statistics: minimum, + maximum, mean, standard deviation, NMAD, total count, and percentage of valid points. + To compute all available statistics, set `stats_name` to `all`. + + :param stats_name: Name or list of names of the statistics to retrieve. If None, main statistics are returned. Accepted names include: `mean`, `median`, `max`, `min`, `sum`, `sum of squares`, `90th percentile`, `iqr`, `LE90`, `nmad`, `rmse`, `std`, `valid count`, `total count`, `percentage valid points` and if an inlier mask is passed : `valid inlier count`, `total inlier count`, `percentage inlier point`, `percentage valid inlier points`. Custom callables can also be provided. + To compute all available statistics, set `stats_name` to `all`. :param inlier_mask: Mask or boolean array of areas to include (inliers=True). :param band: The index of the band for which to compute statistics. Default is 1. :param counts: (number of finite data points in the array, number of valid points (=True, to keep) @@ -784,7 +789,7 @@ def get_stats( return rast.get_stats(stats_name=stats_name, band=band, counts=(valid_points, inlier_points)) # Given list or all attributes to compute if None - if isinstance(stats_name, list) or stats_name is None: + if isinstance(stats_name, list) or stats_name is None or stats_name == "all": return _statistics(data, stats_name, counts) # type: ignore else: # Single attribute to compute @@ -880,7 +885,6 @@ def _raster_equal_allclose( if not complete_equality and warn_failure_reason: where_fail = np.nonzero(~np.array(equalities))[0] - print(f"Equality failed for: {', '.join([names[w] for w in where_fail])}.") warnings.warn( category=UserWarning, message=f"Equality failed for: {', '.join([names[w] for w in where_fail])}." ) diff --git a/geoutils/stats/stats.py b/geoutils/stats/stats.py index be28c832..ef2119da 100644 --- a/geoutils/stats/stats.py +++ b/geoutils/stats/stats.py @@ -33,69 +33,70 @@ from geoutils._typing import NDArrayNum from geoutils.stats.estimators import linear_error, nmad, rmse, sum_square -_STATS_ALIASES = { +_STATS_ALIAS_CALLABLE = { "mean": "Mean", "median": "Median", "max": "Max", - "maximum": "Max", "min": "Min", - "minimum": "Min", "sum": "Sum", "sumofsquares": "Sum of squares", - "sum2": "Sum of squares", "90thpercentile": "90th percentile", - "90percentile": "90th percentile", "iqr": "IQR", "le90": "LE90", "nmad": "NMAD", "rmse": "RMSE", - "rms": "RMSE", "std": "Standard deviation", - "standarddeviation": "Standard deviation", +} + +_STATS_ALIAS_COUNTS = { "validcount": "Valid count", "totalcount": "Total count", "percentagevalidpoints": "Percentage valid points", -} # type: ignore +} -STATS_LIST = [ - "Mean", - "Median", - "Max", - "Min", - "Sum", - "Sum of squares", - "90th percentile", - "IQR", - "LE90", - "NMAD", - "RMSE", - "Standard deviation", - "Valid count", - "Total count", - "Percentage valid points", -] +_STATS_ALIAS_GEN = _STATS_ALIAS_CALLABLE | _STATS_ALIAS_COUNTS -STATS_LIST_MASK = [ - "Valid inlier count", - "Total inlier count", - "Percentage inlier points", - "Percentage valid inlier points", -] +_SYNONYMES = { + "maximum": "max", + "minimum": "min", + "sum": "Sum", + "sum2": "sumofsquares", + "90percentile": "90thpercentile", + "rms": "rmse", + "standarddeviation": "std", +} -_ALIAS_STATS_LIST_MASK = { +_STATS_ALIAS_MASK = { "validinliercount": "Valid inlier count", "totalinliercount": "Total inlier count", "percentagevalidinlierpoints": "Percentage valid inlier points", "percentageinlierpoints": "Percentage inlier points", -} +} # type: ignore + -_ALIAS_STATS = _STATS_ALIASES | _ALIAS_STATS_LIST_MASK +_STATS_ALIAS_ALL = _STATS_ALIAS_GEN | _STATS_ALIAS_MASK +_ALIAS_STATS_GEN = {v: k for k, v in _STATS_ALIAS_GEN.items()} +_ALIAS_STATS_MASK = {v: k for k, v in _STATS_ALIAS_MASK.items()} +_ALIAS_STATS_ALL = _ALIAS_STATS_GEN | _ALIAS_STATS_MASK + + +_STATS_LIST_MIN = [ + "min", + "max", + "mean", + "median", + "std", + "nmad", + "validcount", + "totalcount", + "percentagevalidpoints", +] @profiler.profile("geoutils.stats.stats._statistics", memprof=True) def _statistics( data: NDArrayNum, - stats_name: list[str | Callable[[NDArrayNum], np.floating[Any]]] | None = None, + stats_name: list[str | Callable[[NDArrayNum], np.floating[Any]]] | str | None = None, counts: tuple[int, int] | None = None, ) -> dict[str, float]: """ @@ -161,18 +162,18 @@ def _statistics( valid_count = final_count_nonzero if counts is None else counts[0] stats_dict = { - "Mean": np.ma.mean, - "Median": np.ma.median, - "Max": np.ma.max, - "Min": np.ma.min, - "Sum": np.ma.sum, - "Sum of squares": sum_square, - "90th percentile": partial(lambda x: mquantiles(x, prob=0.9, alphap=1, betap=1)[0]), - "LE90": partial(linear_error, interval=90), - "IQR": partial(iqr, nan_policy="omit"), # ignore masked value (nan), - "NMAD": nmad, - "RMSE": rmse, - "Standard deviation": np.ma.std, + "mean": np.ma.mean, + "median": np.ma.median, + "max": np.ma.max, + "min": np.ma.min, + "sum": np.ma.sum, + "sumofsquares": sum_square, + "90thpercentile": partial(lambda x: mquantiles(x, prob=0.9, alphap=1, betap=1)[0]), + "le90": partial(linear_error, interval=90), + "iqr": partial(iqr, nan_policy="omit"), # ignore masked value (nan), + "nmad": nmad, + "rmse": rmse, + "std": np.ma.std, } # type: ignore else: @@ -184,101 +185,115 @@ def _statistics( valid_count = final_count_nonzero if counts is None else counts[0] stats_dict = { - "Mean": np.nanmean, - "Median": np.nanmedian, - "Max": np.nanmax, - "Min": np.nanmin, - "Sum": np.nansum, - "Sum of squares": sum_square, - "90th percentile": partial(np.nanpercentile, q=90), - "LE90": partial(linear_error, interval=90), - "IQR": partial(iqr, nan_policy="omit"), # ignore masked value (nan), - "NMAD": nmad, - "RMSE": rmse, - "Standard deviation": np.nanstd, + "mean": np.nanmean, + "median": np.nanmedian, + "max": np.nanmax, + "min": np.nanmin, + "sum": np.nansum, + "sumofsquares": sum_square, + "90thpercentile": partial(np.nanpercentile, q=90), + "le90": partial(linear_error, interval=90), + "iqr": partial(iqr, nan_policy="omit"), # ignore masked value (nan), + "nmad": nmad, + "rmse": rmse, + "std": np.nanstd, } # type: ignore # Pixels counts stats_dict.update( { - "Valid count": valid_count, - "Total count": data.size, - "Percentage valid points": (valid_count / data.size) * 100 if data.size else np.nan, + "validcount": valid_count, + "totalcount": data.size, + "percentagevalidpoints": (valid_count / data.size) * 100 if data.size else np.nan, } ) if counts is not None: stats_dict.update( { - "Valid inlier count": final_count_nonzero, - "Total inlier count": counts[1], - "Percentage inlier points": (final_count_nonzero / counts[0]) * 100, - "Percentage valid inlier points": (final_count_nonzero / counts[1]) * 100 if counts[1] != 0 else 0, + "validinliercount": final_count_nonzero, + "totalinliercount": counts[1], + "percentageinlierpoints": (final_count_nonzero / counts[0]) * 100, + "percentagevalidinlierpoints": (final_count_nonzero / counts[1]) * 100 if counts[1] != 0 else 0, } ) - def get_stat_common_name(stat_name: str) -> str | None: + def get_stat_common_alias(stat_name: str) -> str | None: if stat_name in stats_dict.keys(): return stat_name - elif "".join(stat_name.lower().split()) in _ALIAS_STATS.keys(): - return _ALIAS_STATS["".join(stat_name.lower().split())] - elif "".join(stat_name.lower().split("_")) in _ALIAS_STATS.keys(): - return _ALIAS_STATS["".join(stat_name.lower().split("_"))] else: + for split_v in [None, "_"]: + if "".join(stat_name.lower().split(split_v)) in _STATS_ALIAS_ALL.keys(): + return "".join(stat_name.lower().split(split_v)) + if "".join(stat_name.lower().split(split_v)) in _ALIAS_STATS_ALL.keys(): + return _ALIAS_STATS_GEN["".join(stat_name.lower().split(split_v))] + elif "".join(stat_name.lower().split(split_v)) in _SYNONYMES: + return _SYNONYMES["".join(stat_name.lower().split(split_v))] + return None - # If there are no valid data points, set all statistics to NaN + def create_list(counts_is_none: bool, stats_name: str | None) -> list[str]: + if isinstance(stats_name, list): + return stats_name + else: + if stats_name is None: + stat_names_res = _STATS_LIST_MIN + else: + stat_names_res = list(_STATS_ALIAS_GEN) + if counts_is_none: + stat_names_res = stat_names_res + list(_STATS_ALIAS_MASK.keys()) + return stat_names_res + + # If there are no valid data points, raise a warning if final_count_nonzero == 0: warnings.warn("Empty raster, returns Nan for all stats", category=UserWarning) - if stats_name is None: - stat_data_valid = STATS_LIST # type: ignore - if counts is not None: - stat_data_valid = stat_data_valid + STATS_LIST_MASK - else: - stat_data_valid = stats_name # type: ignore + + if stats_name is None or stats_name == "all": + stat_names_res = create_list(counts is not None, stats_name) # type: ignore res_dict = { - stat_name: ( - stats_dict[get_stat_common_name(stat_name)] # type: ignore - if ( - get_stat_common_name(stat_name) is not None - and not callable(stats_dict[get_stat_common_name(stat_name)]) # type: ignore + _STATS_ALIAS_ALL[stat_name]: ( + stats_dict[stat_name](data) # type: ignore + if (callable(stats_dict[stat_name]) and final_count_nonzero != 0) + else ( + np.nan + # If there are no valid data points, set callable statistics to NaN + if (callable(stats_dict[stat_name]) and final_count_nonzero == 0) + else stats_dict[stat_name] ) - else np.nan ) - for stat_name in stat_data_valid + for stat_name in stat_names_res } # type: ignore else: - if stats_name is None: - res_dict = stats_dict # type: ignore - if stats_name is None: - for key in stats_dict.keys(): - if callable(stats_dict[key]): - res_dict[key] = stats_dict[key](data) # type: ignore - else: - res_dict = {} # type: ignore - for stat_name in stats_name: - # Compute stat if in stats_dict keys - if isinstance(stat_name, str): - stat_common_name = get_stat_common_name(stat_name) - if stat_common_name: - if callable(stats_dict[stat_common_name]): - res_dict[stat_name] = stats_dict[stat_common_name](data) # type: ignore - else: - res_dict[stat_name] = stats_dict[stat_common_name] # type: ignore + res_dict = {} # type: ignore + for stat_name in stats_name: + + # Compute stat if in stats_dict keys + if isinstance(stat_name, str): + + # Get common alias + stat_common_alias = get_stat_common_alias(stat_name) + if stat_common_alias: + if stat_common_alias in stats_dict: + res_dict[stat_name] = stats_dict[stat_common_alias] + if callable(res_dict[stat_name]): + if final_count_nonzero == 0: + res_dict[stat_name] = np.nan + else: + res_dict[stat_name] = res_dict[stat_name](data) # type: ignore else: - warnings.warn("Statistic name " + stat_name + " is not recognized", category=UserWarning) - res_dict[stat_name] = np.float32(np.nan) # type: ignore + res_dict[stat_name] = np.nan + else: + warnings.warn("Statistic name " + stat_name + " is not recognized", category=UserWarning) + res_dict[stat_name] = np.float32(np.nan) # type: ignore - # Compute stat if callable - elif callable(stat_name): - res_dict[stat_name.__name__] = stat_name(data) # type: ignore + # Compute stat if callable + elif callable(stat_name): + res_dict[stat_name.__name__] = stat_name(data) # type: ignore - else: - # if none of the above conditions are met and if stats_name is not about the inlier mask - if stat_name not in STATS_LIST_MASK and stat_name not in _ALIAS_STATS_LIST_MASK: - warnings.warn("Statistic name " + stat_name + " is not recognized", category=UserWarning) - res_dict[stat_name] = np.float32(np.nan) # type: ignore + else: + warnings.warn("Statistic name " + stat_name + " is not recognized", category=UserWarning) + res_dict[stat_name] = np.float32(np.nan) # type: ignore - return res_dict # type: ignore + return {k: (v.item() if isinstance(v, np.generic) else v) for k, v in res_dict.items()} # type: ignore diff --git a/tests/test_stats/test_stats.py b/tests/test_stats/test_stats.py index 6abf2a6a..a0f500b2 100644 --- a/tests/test_stats/test_stats.py +++ b/tests/test_stats/test_stats.py @@ -11,34 +11,13 @@ import geoutils as gu from geoutils import examples from geoutils._typing import NDArrayNum - -expected_stats = [ - "Mean", - "Median", - "Max", - "Min", - "Sum", - "Sum of squares", - "90th percentile", - "IQR", - "LE90", - "NMAD", - "RMSE", - "Standard deviation", -] - -expected_stats_count = [ - "Valid count", - "Total count", - "Percentage valid points", -] - -expected_stats_mask = [ - "Valid inlier count", - "Total inlier count", - "Percentage inlier points", - "Percentage valid inlier points", -] +from geoutils.stats.stats import ( + _STATS_ALIAS_ALL, + _STATS_ALIAS_CALLABLE, + _STATS_ALIAS_GEN, + _STATS_ALIAS_MASK, + _STATS_LIST_MIN, +) stat_types = (int, float, np.integer, np.floating) @@ -50,7 +29,8 @@ def compare_dict(dict1: dict, dict2: dict) -> None: # type: ignore if dict1[key] is not np.nan: assert dict2[key] == pytest.approx(dict1[key], abs=1e-10) else: - assert dict2[key] is np.nan + print(dict1[key], dict2[key]) + assert isnan(dict2[key]) class TestStats: @@ -66,49 +46,77 @@ def test_get_stats_raster(self, example: str) -> None: """ raster = gu.Raster(example) - # Full stats + # Default stats stats = raster.get_stats() - assert len(stats) == len(expected_stats + expected_stats_count) + assert len(stats) == len(_STATS_LIST_MIN) + assert list(stats.keys()) == [_STATS_ALIAS_ALL[key] for key in _STATS_LIST_MIN] + for name in _STATS_LIST_MIN: + assert _STATS_ALIAS_ALL[name] in stats + assert isinstance(stats.get(_STATS_ALIAS_ALL[name]), stat_types) - for name in expected_stats + expected_stats_count: + # Full stats + stats = raster.get_stats("all") + assert len(stats) == len(_STATS_ALIAS_GEN) + for name in _STATS_ALIAS_GEN.values(): assert name in stats assert isinstance(stats.get(name), stat_types) # With mask (inlier=True) inlier_mask = ~raster.get_mask() - stats_masked = raster.get_stats(inlier_mask=inlier_mask) - assert len(stats_masked) == len(expected_stats + expected_stats_count + expected_stats_mask) - for name in expected_stats_mask: + stats_masked = raster.get_stats("all", inlier_mask=inlier_mask) + assert len(stats_masked) == len(_STATS_ALIAS_ALL) + assert list(stats_masked.keys()) == [_STATS_ALIAS_ALL[key] for key in _STATS_ALIAS_ALL] + for name in _STATS_ALIAS_MASK.values(): assert name in stats_masked stats_masked.pop(name) assert stats_masked == stats + # Print of the values + stats = raster.get_stats("all", inlier_mask=inlier_mask) + for stat in stats: + assert not isinstance(stat, np.generic) + for stat in _STATS_ALIAS_ALL: + assert not isinstance(raster.get_stats(stat, inlier_mask=inlier_mask), np.generic) + + # With mask (inlier=True) and default list + stats_masked = raster.get_stats(inlier_mask=inlier_mask) + assert len(stats_masked) == len(_STATS_LIST_MIN) + assert list(stats_masked.keys()) == [_STATS_ALIAS_ALL[key] for key in _STATS_LIST_MIN] + for name in _STATS_LIST_MIN: + assert _STATS_ALIAS_ALL[name] in stats_masked + # Test case sensitive + space/underscore possibilities stats_masked = raster.get_stats(inlier_mask=inlier_mask) - for name in expected_stats + expected_stats_count + expected_stats_mask: - assert stats_masked[name] == raster.get_stats(stats_name=name.lower(), inlier_mask=inlier_mask) - assert stats_masked[name] == raster.get_stats(stats_name="".join(name.split()), inlier_mask=inlier_mask) - assert stats_masked[name] == raster.get_stats( - stats_name="".join(name.lower().split()), inlier_mask=inlier_mask - ) - assert stats_masked[name] == raster.get_stats(stats_name="_".join(name.split()), inlier_mask=inlier_mask) - assert stats_masked[name] == raster.get_stats( - stats_name="_".join(name.lower().split()), inlier_mask=inlier_mask - ) + name = "Standard deviation" + assert stats_masked["Standard deviation"] == raster.get_stats( + stats_name="standard deviation", inlier_mask=inlier_mask + ) + assert stats_masked["Standard deviation"] == raster.get_stats( + stats_name="standarddeviation", inlier_mask=inlier_mask + ) + assert stats_masked["Standard deviation"] == raster.get_stats( + stats_name="standard_deviation", inlier_mask=inlier_mask + ) + assert stats_masked[name] == raster.get_stats(stats_name="standard_deviation", inlier_mask=inlier_mask) # Empty mask (=False) empty_mask = np.zeros_like(inlier_mask) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning, message="Empty raster") - stats_masked = raster.get_stats(inlier_mask=empty_mask) - assert len(stats_masked) == len(expected_stats + expected_stats_count + expected_stats_mask) - for name in expected_stats: + stats_masked = raster.get_stats("all", inlier_mask=empty_mask) + assert len(stats_masked) == len(_STATS_ALIAS_ALL) + for name in _STATS_ALIAS_CALLABLE.values(): assert np.isnan(stats_masked.get(name)) assert stats_masked.get("Valid count") == stats.get("Valid count") assert stats_masked.get("Total count") == stats.get("Total count") assert stats_masked.get("Percentage valid points") == stats.get("Percentage valid points") + for stat in _STATS_ALIAS_ALL: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning, message="Empty raster") + stats_masked = raster.get_stats(inlier_mask=empty_mask, stats_name=stat) + with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning, message="Empty raster") stats_masked = raster.get_stats(inlier_mask=empty_mask, stats_name="mean") @@ -123,8 +131,8 @@ def test_get_stats_raster(self, example: str) -> None: assert stats_masked == 0 with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning, message="Empty raster") - stats_masked = raster.get_stats(inlier_mask=inlier_mask) - for name in expected_stats + expected_stats_count + expected_stats_mask: + stats_masked = raster.get_stats("all", inlier_mask=inlier_mask) + for name in stats_masked: assert stats_masked[name] == raster.get_stats(stats_name=name.lower(), inlier_mask=inlier_mask) assert stats_masked[name] == raster.get_stats(stats_name="".join(name.split()), inlier_mask=inlier_mask) @@ -136,18 +144,21 @@ def test_get_stats_raster(self, example: str) -> None: ) with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning, message="Empty raster") - stats_empty = dem_empty.get_stats() - assert len(stats_empty) == len(expected_stats + expected_stats_count) - for name in expected_stats: + stats_empty = dem_empty.get_stats("all") + assert len(stats_empty) == len(_STATS_ALIAS_GEN) + for name in _STATS_ALIAS_CALLABLE.values(): assert np.isnan(stats_empty.get(name)) assert stats_empty.get("Valid count") == 0 assert stats_empty.get("Total count") == 0 assert isnan(stats_empty.get("Percentage valid points")) # Single stat - for name in expected_stats + expected_stats_count: + for name in _STATS_ALIAS_GEN: stat = raster.get_stats(stats_name=name) assert np.isfinite(stat) + for name in _STATS_ALIAS_MASK: + stat = raster.get_stats(stats_name=name) + assert np.isnan(stat) # Alias stat assert raster.get_stats(stats_name="Valid count") == raster.get_stats(stats_name="valid_count") @@ -197,18 +208,38 @@ def test_get_stats_raster_pointcloud(self, example: str) -> None: parameters. """ raster = gu.Raster(example) + pointcloud = raster.to_pointcloud() + + # Default stats + stats = pointcloud.get_stats() + assert len(stats) == len(_STATS_LIST_MIN) + assert list(stats.keys()) == [_STATS_ALIAS_ALL[key] for key in _STATS_LIST_MIN] + for name in _STATS_LIST_MIN: + assert _STATS_ALIAS_ALL[name] in stats + assert isinstance(stats.get(_STATS_ALIAS_GEN[name]), stat_types) # Full stats - stats = raster.to_pointcloud().get_stats() - assert len(stats) == len(expected_stats + expected_stats_count) - for name in expected_stats + expected_stats_count: + stats = pointcloud.get_stats("all") + assert len(stats) == len(_STATS_ALIAS_GEN) + assert list(stats.keys()) == [_STATS_ALIAS_GEN[key] for key in _STATS_ALIAS_GEN] + for name in _STATS_ALIAS_GEN.values(): assert name in stats assert isinstance(stats.get(name), stat_types) # Single stat - for name in expected_stats + expected_stats_count: - stat = raster.to_pointcloud().get_stats(stats_name=name) + for name in _STATS_ALIAS_GEN: + stat = pointcloud.get_stats(stats_name=name) assert np.isfinite(stat) + for name in _STATS_ALIAS_MASK: + stat = pointcloud.get_stats(stats_name=name) + assert np.isnan(stat) + + # Print of the values + stats = pointcloud.get_stats("all") + for stat in stats: + assert not isinstance(stat, np.generic) + for stat in _STATS_ALIAS_ALL: + assert not isinstance(pointcloud.get_stats(stat), np.generic) # Callable def percentile_95(data: NDArrayNum) -> np.floating[Any]: @@ -218,7 +249,7 @@ def percentile_95(data: NDArrayNum) -> np.floating[Any]: # Selected stats and callable stats_name = ["mean", "max", "std", "percentile_95"] - stats = raster.to_pointcloud().get_stats(stats_name=["mean", "max", "std", percentile_95]) + stats = pointcloud.get_stats(stats_name=["mean", "max", "std", percentile_95]) assert len(stats) == len(stats_name) for name in stats_name: assert name in stats @@ -227,23 +258,26 @@ def percentile_95(data: NDArrayNum) -> np.floating[Any]: # Non-existing stats with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="Statistic name 80 percentile is not recognized") - stat = raster.get_stats(stats_name="80 percentile") - assert isnan(stat) + stat = pointcloud.get_stats(stats_name="80 percentile") + assert isnan(stat) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="Statistic name 42 is a not recognized string") - stat = raster.get_stats(stats_name=42) - assert stat is None + stat = pointcloud.get_stats(stats_name=42) + assert stat is None # Empty mask (=False) inlier_mask = ~raster.get_mask() + inlier_mask = ~raster.get_mask() empty_mask = np.zeros_like(inlier_mask) raster.set_mask(~empty_mask) + pointcloud = raster.to_pointcloud() with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="Empty raster") - stats_masked = raster.to_pointcloud().get_stats() - assert len(stats_masked) == len(expected_stats + expected_stats_count) - for name in expected_stats: + stats_masked = pointcloud.get_stats("all") + assert len(stats_masked) == len(_STATS_ALIAS_GEN) + for name in _STATS_ALIAS_CALLABLE.values(): + assert np.isnan(stats_masked.get(name)) assert stats_masked.get("Valid count") == 0 assert stats_masked.get("Total count") == 0 @@ -277,7 +311,7 @@ def test_raster_get_stats_values(self) -> None: "Total count": 524000, "Percentage valid points": np.float64(100.0), } - compare_dict(res_stats, rast.get_stats()) + compare_dict(res_stats, rast.get_stats("all")) # Verify raster stats with a mask res_stats_mask = { @@ -301,7 +335,7 @@ def test_raster_get_stats_values(self) -> None: "Percentage inlier points": np.float64(46.03015267175572), "Percentage valid inlier points": np.float64(100.0), } - compare_dict(res_stats_mask, rast.get_stats(inlier_mask=inlier_mask)) + compare_dict(res_stats_mask, rast.get_stats("all", inlier_mask=inlier_mask)) # Verify cropped raster nrows, ncols = rast.shape @@ -323,7 +357,7 @@ def test_raster_get_stats_values(self) -> None: "Total count": 273000, "Percentage valid points": np.float64(100.0), } - compare_dict(res_stats_crop, rast_crop.get_stats()) + compare_dict(res_stats_crop, rast_crop.get_stats("all")) # Verify reprojected raster with warnings.catch_warnings(): @@ -347,7 +381,7 @@ def test_raster_get_stats_values(self) -> None: "Total count": 524000, "Percentage valid points": np.float64(40.36774809160305), } - compare_dict(res_stats_crop_proj, rast_crop_proj.get_stats()) + compare_dict(res_stats_crop_proj, rast_crop_proj.get_stats("all")) # Verify stats of a masked raster rast.set_mask(inlier_mask) @@ -368,7 +402,7 @@ def test_raster_get_stats_values(self) -> None: "Total count": 524000, "Percentage valid points": np.float64(53.96984732824428), } - compare_dict(stats_masked_rast, rast.get_stats()) + compare_dict(stats_masked_rast, rast.get_stats("all")) # Verify stats of a masked raster with the other part covered by the inler_mask (=> empty raster) stats_masked_rast_masked = { @@ -394,7 +428,7 @@ def test_raster_get_stats_values(self) -> None: } with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning, message="Empty raster") - compare_dict(stats_masked_rast_masked, rast.get_stats(inlier_mask=inlier_mask)) + compare_dict(stats_masked_rast_masked, rast.get_stats("all", inlier_mask=inlier_mask)) def test_pointcloud_get_stats_values(self) -> None: """ @@ -423,7 +457,7 @@ def test_pointcloud_get_stats_values(self) -> None: "Total count": 524000, "Percentage valid points": np.float64(100.0), } - compare_dict(rast_stats_pc, rast_pc.get_stats()) + compare_dict(rast_stats_pc, rast_pc.get_stats("all")) # Verify cropped raster pc nrows, ncols = rast.shape @@ -447,7 +481,7 @@ def test_pointcloud_get_stats_values(self) -> None: "Total count": 273000, "Percentage valid points": np.float64(100.0), } - compare_dict(rast_stats_crop_pc, rast_crop_pc.get_stats()) + compare_dict(rast_stats_crop_pc, rast_crop_pc.get_stats("all")) # Verify reprojected raster pc with warnings.catch_warnings(): @@ -473,4 +507,4 @@ def test_pointcloud_get_stats_values(self) -> None: "Total count": 211527, "Percentage valid points": np.float64(100.0), } - compare_dict(rast_stats_crop_proj_pc, rast_crop_proj_pc.get_stats()) + compare_dict(rast_stats_crop_proj_pc, rast_crop_proj_pc.get_stats("all"))