diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d683702..3f71d69 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,4 +20,4 @@ jobs: - name: Install package run: python -m pip install .[opensimplex] - name: Test package - run: python -c "import topotoolbox as topo; dem = topo.GridObject.gen_random(); assert (dem.fillsinks() >= dem).z.all()" + run: python -c "import topotoolbox as topo; dem = topo.gen_random(); assert (dem.fillsinks() >= dem).z.all()" diff --git a/docs/Makefile b/docs/Makefile index 285325e..fbf9711 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -9,7 +9,8 @@ SOURCEDIR = . BUILDDIR = _build EXAMPLEDIR = ../examples -DOCEXAMPLEDIR = $(SOURCEDIR)/_examples +DOCEXAMPLEDIR = _examples +AUTOSUMMARYDIR = _autosummary # Put it first so that "make" without argument is like "make help". help: @@ -22,8 +23,18 @@ html: @echo "Copying examples directory..." @cp -r $(EXAMPLEDIR) $(DOCEXAMPLEDIR) @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - @echo "Cleaning up examples directory..." - @rm -rf $(DOCEXAMPLEDIR) + @echo "Cleaning up _examples directory..." + @rm -rf $(DOCEXAMPLEDIR)/* + @echo "Cleaning up _autosummary directory..." + @rm -rf $(AUTOSUMMARYDIR)/* + +clean: + @echo "Removing everything under '_build'..." + @rm -rf $(BUILDDIR)/* + @echo "Cleaning up '_autosummary' directory..." + @rm -rf $(AUTOSUMMARYDIR)/* + @echo "Cleaning up '_examples' directory..." + @rm -rf $(DOCEXAMPLEDIR)/* # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). diff --git a/docs/api.rst b/docs/api.rst index 05f9cd2..2474912 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5,4 +5,11 @@ API Documentation :toctree: _autosummary :recursive: - topotoolbox \ No newline at end of file + topotoolbox.GridObject + topotoolbox.read_tif + topotoolbox.load_dem + topotoolbox.get_dem_names + topotoolbox.get_cache_contents + topotoolbox.clear_cache + topotoolbox.gen_random + topotoolbox.gen_random_bool diff --git a/examples/example_magic_funcs.ipynb b/examples/example_magic_funcs.ipynb index 8045d3f..c2fb607 100644 --- a/examples/example_magic_funcs.ipynb +++ b/examples/example_magic_funcs.ipynb @@ -31,7 +31,7 @@ "metadata": {}, "outputs": [], "source": [ - "dem = topo.GridObject.gen_random_bool(rows=4, columns=4)\n", + "dem = topo.gen_random_bool(rows=4, columns=4)\n", "for i in dem:\n", " print(i)\n", "\n", @@ -59,8 +59,8 @@ "metadata": {}, "outputs": [], "source": [ - "dem1 = topo.GridObject.gen_random(rows=32, columns=32, hillsize=24)\n", - "dem2 = topo.GridObject.gen_random(rows=32, columns=32, hillsize=32)\n", + "dem1 = topo.gen_random(rows=32, columns=32, hillsize=24)\n", + "dem2 = topo.gen_random(rows=32, columns=32, hillsize=32)\n", "\n", "fig, (ax1, ax2, ax3, ax4, ax5, ax6) = plt.subplots(1, 6, figsize=(14,4))\n", "im1 = ax1.imshow(dem1, cmap='terrain')\n", @@ -86,8 +86,8 @@ "metadata": {}, "outputs": [], "source": [ - "dem1 = topo.GridObject.gen_random_bool()\n", - "dem2 = topo.GridObject.gen_random_bool()\n", + "dem1 = topo.gen_random_bool()\n", + "dem2 = topo.gen_random_bool()\n", "\n", "fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(1, 5, figsize=(12,4))\n", "im1 = ax1.imshow(dem1, cmap='binary')\n", @@ -118,8 +118,8 @@ "metadata": {}, "outputs": [], "source": [ - "dem1 = topo.GridObject.gen_random(rows=32, columns=32, hillsize=32)\n", - "dem2 = topo.GridObject.gen_random(rows=32, columns=32, hillsize=16)\n", + "dem1 = topo.gen_random(rows=32, columns=32, hillsize=32)\n", + "dem2 = topo.gen_random(rows=32, columns=32, hillsize=16)\n", "\n", "fig, (ax1, ax2, ax3) = plt.subplots(1, 3)\n", "ax1.imshow(dem1, cmap='terrain')\n", diff --git a/examples/test_GridObject.ipynb b/examples/test_GridObject.ipynb index 4b224c1..c5169a8 100644 --- a/examples/test_GridObject.ipynb +++ b/examples/test_GridObject.ipynb @@ -24,7 +24,7 @@ "metadata": {}, "outputs": [], "source": [ - "dem = topo.GridObject.gen_random()\n", + "dem = topo.gen_random()\n", "dem.info()" ] }, diff --git a/examples/test_genGrid.ipynb b/examples/test_genGrid.ipynb index 6b828f9..6719c87 100644 --- a/examples/test_genGrid.ipynb +++ b/examples/test_genGrid.ipynb @@ -18,7 +18,7 @@ "from matplotlib.colors import LightSource\n", "\n", "# generating Gridobject with random terrain\n", - "dem = topo.GridObject.gen_random()\n", + "dem = topo.gen_random()\n", "\n", "# plotting dem with hillshade\n", "ls = LightSource(azdeg=270, altdeg=60)\n", diff --git a/src/topotoolbox/__init__.py b/src/topotoolbox/__init__.py index f6cf69b..cc6a100 100644 --- a/src/topotoolbox/__init__.py +++ b/src/topotoolbox/__init__.py @@ -1,2 +1,2 @@ -from .grid_object import GridObject +from .grid_object import * from .utils import * diff --git a/src/topotoolbox/grid_object.py b/src/topotoolbox/grid_object.py index e2b328b..f44a195 100644 --- a/src/topotoolbox/grid_object.py +++ b/src/topotoolbox/grid_object.py @@ -1,147 +1,339 @@ """This module contains the GridObject class. """ -import random -from typing import Union +import copy import numpy as np -import rasterio -from .gridmixins import * # pylint: disable=W0401 +# pylint: disable=import-error +from ._grid import grid_fillsinks # type: ignore +from ._grid import grid_identifyflats # type: ignore __all__ = ['GridObject'] -class GridObject( - InfoMixin, - FillsinksMixin, - IdentifyflatsMixin, - MagicMixin -): +class GridObject(): """A class containing all information of a Digital Elevation Model (DEM). - This class combines mixins to provide various functionalities - for working with DEMs. """ - def __init__(self, path: Union[str, None] = None) -> None: + def __init__(self) -> None: """Initialize a GridObject instance. + """ + + self.path = '' + self.z = np.empty(()) + self.rows = 0 + self.columns = 0 + self.shape = self.z.shape + self.cellsize = 0 - Args: - path (str, optional): The path to the raster file. - Defaults to None. + def fillsinks(self): + """Fill sinks in the digital elevation model (DEM). - Raises: - TypeError: If an invalid type is passed as the `path`. - ValueError: If an error occurs while processing - the `path` argument. + Returns + ------- + GridObject + The filled DEM. """ - if path is not None: - try: - dataset = rasterio.open(path) - - except TypeError as err: - raise TypeError(err) from None - except Exception as err: - raise ValueError(err) from None - - self.path = path - self.z = dataset.read(1).astype(np.float32) - self.rows = dataset.height - self.columns = dataset.width - self.shape = self.z.shape - self.cellsize = dataset.res[0] - - else: - self.path = '' - self.z = np.empty(()) - self.rows = 0 - self.columns = 0 - self.shape = self.z.shape - self.cellsize = 0 - - @classmethod - def gen_random( - cls, hillsize: int = 24, rows: int = 128, columns: int = 128, - cellsize: float = 10.0) -> 'GridObject': - """Generate a GridObject instance that is generated with - OpenSimplex noise. - - Args: - hillsize (int, optional): Controls the "smoothness" of the - generated terrain. Defaults to 24. - rows (int, optional): Number of rows. Defaults to 128. - columns (int, optional): Number of columns. Defaults to 128. - cellsize (float, optional): Size of each cell in the grid. - Defaults to 10.0. - - Raises: - ImportError: If OpenSimplex has not been installed. - - Returns: - GridObject: An instance of GridObject with randomly - generated values. + dem = self.z.astype(np.float32) + + output = np.zeros_like(dem) + + grid_fillsinks(output, dem, self.rows, self.columns) + + result = copy.copy(self) + result.z = output + + return result + + def identifyflats( + self, raw: bool = False, output: list[str] = None) -> tuple: + """Identifies flats and sills in a digital elevation model (DEM). + + Parameters + ---------- + raw : bool, optional + If True, returns the raw output grid as np.ndarray. + Defaults to False. + output : list of str, optional + List of strings indicating desired output types. Possible values + are 'sills', 'flats'. Defaults to ['sills', 'flats']. + + Returns + ------- + tuple + A tuple containing copies of the DEM with identified + flats and/or sills. + + Notes + ----- + Flats are identified as 1s, sills as 2s, and presills as 5s + (since they are also flats) in the output grid. + Only relevant when using raw=True. """ - try: - import opensimplex as simplex # pylint: disable=C0415 - - except ImportError: - err = ("For gen_random to work, use \"pip install topotool" + - "box[opensimplex]\" or \"pip install .[opensimplex]\"") - raise ImportError(err) from None - - noise_array = np.empty((rows, columns), dtype=np.float32) - for y in range(0, rows): - for x in range(0, columns): - value = simplex.noise4(x / hillsize, y / hillsize, 0.0, 0.0) - color = int((value + 1) * 128) - noise_array[y, x] = color - - instance = cls(None) - instance.path = '' - instance.z = noise_array - instance.rows = rows - instance.columns = columns - instance.shape = instance.z.shape - instance.cellsize = cellsize - - return instance - - # TODO: implement gen_empty - - @classmethod - def gen_empty(cls) -> None: - pass - - @classmethod - def gen_random_bool( - cls, rows: int = 32, columns: int = 32, cellsize: float = 10.0 - ) -> 'GridObject': - """Generate a GridObject instance that contains only randomly - generated Boolean values. - - Args: - rows (int, optional): Number of rows. Defaults to 32. - columns (int, optional): Number of columns. Defaults to 32. - cellsize (float, optional): size of each cell in the grid. - Defaults to 10. - - Returns: - GridObject: _description_ + if output is None: + output = ['sills', 'flats'] + + dem = self.z.astype(np.float32) + output_grid = np.zeros_like(dem).astype(np.int32) + + grid_identifyflats(output_grid, dem, self.rows, self.columns) + + if raw: + return output_grid + + result = [] + if 'flats' in output: + flats = copy.copy(self) + flats.z = np.zeros_like(flats.z) + flats.z = np.where((output_grid & 1) == 1, 1, flats.z) + result.append(flats) + + if 'sills' in output: + sills = copy.copy(self) + sills.z = np.zeros_like(sills.z) + sills.z = np.where((output_grid & 2) == 2, 1, sills.z) + result.append(sills) + + return tuple(result) + + def info(self): + """Prints all variables of a GridObject. """ - bool_array = np.empty((rows, columns), dtype=np.float32) + print("path: "+str(self.path)) + print("rows: "+str(self.rows)) + print("cols: "+str(self.columns)) + print("cellsize: "+str(self.cellsize)) + + # 'Magic' functions: + + def __eq__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + dem.z[x][y] = self.z[x][y] == other.z[x][y] + + return dem + + def __ne__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + dem.z[x][y] = self.z[x][y] != other.z[x][y] + + return dem + + def __gt__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + dem.z[x][y] = self.z[x][y] > other.z[x][y] + + return dem + + def __lt__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + dem.z[x][y] = self.z[x][y] < other.z[x][y] + + return dem + + def __ge__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + dem.z[x][y] = self.z[x][y] >= other.z[x][y] + + return dem + + def __le__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + dem.z[x][y] = self.z[x][y] <= other.z[x][y] + + return dem + + def __add__(self, other): + dem = copy.copy(self) + + if isinstance(other, self.__class__): + dem.z = self.z + other.z + return dem + + dem.z = self.z + other + return dem + + def __sub__(self, other): + dem = copy.copy(self) + + if isinstance(other, self.__class__): + dem.z = self.z - other.z + return dem + + dem.z = self.z - other + return dem + + def __mul__(self, other): + dem = copy.copy(self) + + if isinstance(other, self.__class__): + dem.z = self.z * other.z + return dem + + dem.z = self.z * other + return dem + + def __div__(self, other): + dem = copy.copy(self) + + if isinstance(other, self.__class__): + dem.z = self.z / other.z + return dem + + dem.z = self.z / other + return dem + + def __and__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + if (self.z[x][y] not in [0, 1] + or other.z[x][y] not in [0, 1]): + + raise ValueError( + "Invalid cell value. 'and' can only compare " + + "True (1) and False (0) values.") + + dem.z[x][y] = (int(self.z[x][y]) & int(other.z[x][y])) + + return dem + + def __or__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + if (self.z[x][y] not in [0, 1] + or other.z[x][y] not in [0, 1]): + + raise ValueError( + "Invalid cell value. 'or' can only compare True (1)" + + " and False (0) values.") + + dem.z[x][y] = (int(self.z[x][y]) | int(other.z[x][y])) + + return dem + + def __xor__(self, other): + dem = copy.deepcopy(self) + + if not isinstance(other, self.__class__): + raise TypeError("Can only compare two GridObjects.") + + if self.columns != other.columns or self.rows != other.rows: + raise ValueError("Both GridObjects have to be the same size.") + + for x in range(0, self.columns): + for y in range(0, self.rows): + + if (self.z[x][y] not in [0, 1] + or other.z[x][y] not in [0, 1]): + + raise ValueError( + "Invalid cell value. 'xor' can only compare True (1)" + + " and False (0) values.") + + dem.z[x][y] = (int(self.z[x][y]) ^ int(other.z[x][y])) + + return dem + + def __len__(self): + return len(self.z) + + def __iter__(self): + return iter(self.z) + + def __getitem__(self, index): + return self.z[index] + + def __setitem__(self, index, value): + try: + value = np.float32(value) + except: + raise TypeError( + value, " not can't be converted to float32.") from None - for y in range(0, rows): - for x in range(0, columns): - bool_array[x][y] = random.choice([0, 1]) + self.z[index] = value - instance = cls(None) - instance.path = '' - instance.z = bool_array - instance.rows = rows - instance.columns = columns - instance.shape = instance.z.shape - instance.cellsize = cellsize + def __array__(self): + return self.z - return instance + def __str__(self): + return str(self.z) diff --git a/src/topotoolbox/gridmixins/__init__.py b/src/topotoolbox/gridmixins/__init__.py deleted file mode 100644 index 32ddd41..0000000 --- a/src/topotoolbox/gridmixins/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .fillsinks import FillsinksMixin -from .info import InfoMixin -from .magic import MagicMixin -from .identifyflats import IdentifyflatsMixin - -__all__ = ['FillsinksMixin', 'InfoMixin', 'MagicMixin', 'IdentifyflatsMixin'] diff --git a/src/topotoolbox/gridmixins/fillsinks.py b/src/topotoolbox/gridmixins/fillsinks.py deleted file mode 100644 index 2796371..0000000 --- a/src/topotoolbox/gridmixins/fillsinks.py +++ /dev/null @@ -1,32 +0,0 @@ -"""This module contains the Mixin class FillsinksMixin for the GridObject. -""" -import copy - -import numpy as np - -# Importing the wrapper of the fillsinks function provided by pybind11. -from .._grid import grid_fillsinks # pylint: disable=import-error - - -class FillsinksMixin(): - """This class is a Mixin for the GridObject class. - It contains the fillsinks() function. - """ - - def fillsinks(self): - """Fill sinks in the digital elevation model (DEM). - - Returns: - GridObject: The filled DEM. - """ - - dem = self.z.astype(np.float32) - - output = np.zeros_like(dem) - - grid_fillsinks(output, dem, self.rows, self.columns) - - result = copy.copy(self) - result.z = output - - return result diff --git a/src/topotoolbox/gridmixins/identifyflats.py b/src/topotoolbox/gridmixins/identifyflats.py deleted file mode 100644 index 98a88fe..0000000 --- a/src/topotoolbox/gridmixins/identifyflats.py +++ /dev/null @@ -1,59 +0,0 @@ -"""This module contains the Mixin class IdentifyflatsMixin for the GridObject. -""" -import copy - -import numpy as np - -from .._grid import grid_identifyflats # pylint: disable=import-error - - -class IdentifyflatsMixin(): - """Mixin class containing Identifyflats. - """ - - def identifyflats( - self, raw: bool = False, output: list[str] = None) -> tuple: - """ - Identifies flats and sills in a digital elevation model (DEM). - - Args: - raw (bool): If True, returns the raw output grid as np.ndarray. - Defaults to False. - output (list): List of strings indicating desired output types. - Possible values are 'sills', 'flats'. - Defaults to ['sills', 'flats']. - - Returns: - tuple: A tuple containing copies of the DEM with identified - flats and/or sills. - - Note: - Flats are identified as 1s, sills as 2s and presills as 5s - (since they are also flats) in the output grid. - Only relevant when using raw=True. - """ - if output is None: - output = ['sills', 'flats'] - - dem = self.z.astype(np.float32) - output_grid = np.zeros_like(dem).astype(np.int32) - - grid_identifyflats(output_grid, dem, self.rows, self.columns) - - if raw: - return output_grid - - result = [] - if 'flats' in output: - flats = copy.copy(self) - flats.z = np.zeros_like(flats.z) - flats.z = np.where((output_grid & 1) == 1, 1, flats.z) - result.append(flats) - - if 'sills' in output: - sills = copy.copy(self) - sills.z = np.zeros_like(sills.z) - sills.z = np.where((output_grid & 2) == 2, 1, sills.z) - result.append(sills) - - return tuple(result) diff --git a/src/topotoolbox/gridmixins/info.py b/src/topotoolbox/gridmixins/info.py deleted file mode 100644 index 3069d01..0000000 --- a/src/topotoolbox/gridmixins/info.py +++ /dev/null @@ -1,16 +0,0 @@ -"""This module contains the Mixin class InfoMixin for the GridObject. -""" - - -class InfoMixin(): - """This class is a Mixin for the GridObject class. - It contains the info() function. - """ - - def info(self): - """Prints all variables of a GridObject. - """ - print("path: "+str(self.path)) - print("rows: "+str(self.rows)) - print("cols: "+str(self.columns)) - print("cellsize: "+str(self.cellsize)) diff --git a/src/topotoolbox/gridmixins/magic.py b/src/topotoolbox/gridmixins/magic.py deleted file mode 100644 index e931d33..0000000 --- a/src/topotoolbox/gridmixins/magic.py +++ /dev/null @@ -1,239 +0,0 @@ -"""This module contains the Mixin class MagicMixin for the GridObject. -""" -import copy -import numpy as np - - -class MagicMixin(): - """This class is a Mixin for the GridObject, that contains - Magic or Dunder methods for the GridObject. - """ - - def __eq__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - dem.z[x][y] = self.z[x][y] == other.z[x][y] - - return dem - - def __ne__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - dem.z[x][y] = self.z[x][y] != other.z[x][y] - - return dem - - def __gt__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - dem.z[x][y] = self.z[x][y] > other.z[x][y] - - return dem - - def __lt__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - dem.z[x][y] = self.z[x][y] < other.z[x][y] - - return dem - - def __ge__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - dem.z[x][y] = self.z[x][y] >= other.z[x][y] - - return dem - - def __le__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - dem.z[x][y] = self.z[x][y] <= other.z[x][y] - - return dem - - def __add__(self, other): - dem = copy.copy(self) - - if isinstance(other, self.__class__): - dem.z = self.z + other.z - return dem - - dem.z = self.z + other - return dem - - def __sub__(self, other): - dem = copy.copy(self) - - if isinstance(other, self.__class__): - dem.z = self.z - other.z - return dem - - dem.z = self.z - other - return dem - - def __mul__(self, other): - dem = copy.copy(self) - - if isinstance(other, self.__class__): - dem.z = self.z * other.z - return dem - - dem.z = self.z * other - return dem - - def __div__(self, other): - dem = copy.copy(self) - - if isinstance(other, self.__class__): - dem.z = self.z / other.z - return dem - - dem.z = self.z / other - return dem - - def __and__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - if (self.z[x][y] not in [0, 1] - or other.z[x][y] not in [0, 1]): - - raise ValueError( - "Invalid cell value. 'and' can only compare " + - "True (1) and False (0) values.") - - dem.z[x][y] = (int(self.z[x][y]) & int(other.z[x][y])) - - return dem - - def __or__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - if (self.z[x][y] not in [0, 1] - or other.z[x][y] not in [0, 1]): - - raise ValueError( - "Invalid cell value. 'or' can only compare True (1)" + - " and False (0) values.") - - dem.z[x][y] = (int(self.z[x][y]) | int(other.z[x][y])) - - return dem - - def __xor__(self, other): - dem = copy.deepcopy(self) - - if not isinstance(other, self.__class__): - raise TypeError("Can only compare two GridObjects.") - - if self.columns != other.columns or self.rows != other.rows: - raise ValueError("Both GridObjects have to be the same size.") - - for x in range(0, self.columns): - for y in range(0, self.rows): - - if (self.z[x][y] not in [0, 1] - or other.z[x][y] not in [0, 1]): - - raise ValueError( - "Invalid cell value. 'xor' can only compare True (1)" + - " and False (0) values.") - - dem.z[x][y] = (int(self.z[x][y]) ^ int(other.z[x][y])) - - return dem - - def __len__(self): - return len(self.z) - - def __iter__(self): - return iter(self.z) - - def __getitem__(self, index): - return self.z[index] - - def __setitem__(self, index, value): - try: - value = np.float32(value) - except: - raise TypeError( - value, " not can't be converted to float32.") from None - - self.z[index] = value - - def __array__(self): - return self.z - - def __str__(self): - return str(self.z) diff --git a/src/topotoolbox/utils.py b/src/topotoolbox/utils.py index bc2c75a..7f579cc 100644 --- a/src/topotoolbox/utils.py +++ b/src/topotoolbox/utils.py @@ -2,13 +2,17 @@ """ import sys import os +import random from shutil import rmtree from urllib.request import urlopen, urlretrieve +import rasterio +import numpy as np + from .grid_object import GridObject -__all__ = ["load_dem", "get_dem_names", "read_tif", - "get_cache_contents", "clear_cache"] +__all__ = ["load_dem", "get_dem_names", "read_tif", "gen_random", + "gen_random_bool", "get_cache_contents", "clear_cache"] DEM_SOURCE = "https://raw.githubusercontent.com/TopoToolbox/DEMs/master" DEM_NAMES = f"{DEM_SOURCE}/dem_names.txt" @@ -17,21 +21,135 @@ def read_tif(path: str) -> GridObject: """Generate a new GridObject from a .tif file. - Args: - path (str): path to .tif file. + Parameters + ---------- + path : str + path to .tif file. + + Returns + ------- + GridObject + A new GridObject of the .tif file. + """ - Returns: - GridObject: A new GridObject of the .tif file. + grid = GridObject() + + if path is not None: + try: + dataset = rasterio.open(path) + + except TypeError as err: + raise TypeError(err) from None + except Exception as err: + raise ValueError(err) from None + + grid.path = path + grid.z = dataset.read(1).astype(np.float32) + grid.rows = dataset.height + grid.columns = dataset.width + grid.shape = grid.z.shape + grid.cellsize = dataset.res[0] + + return grid + + +def gen_random(hillsize: int = 24, rows: int = 128, columns: int = 128, + cellsize: float = 10.0) -> 'GridObject': + """Generate a GridObject instance that is generated with OpenSimplex noise. + + Parameters + ---------- + hillsize : int, optional + Controls the "smoothness" of the generated terrain. Defaults to 24. + rows : int, optional + Number of rows. Defaults to 128. + columns : int, optional + Number of columns. Defaults to 128. + cellsize : float, optional + Size of each cell in the grid. Defaults to 10.0. + + Raises + ------ + ImportError + If OpenSimplex has not been installed. + + Returns + ------- + GridObject + An instance of GridObject with randomly generated values. """ - return GridObject(path) + try: + import opensimplex as simplex # pylint: disable=C0415 + + except ImportError: + err = ("For gen_random to work, use \"pip install topotool" + + "box[opensimplex]\" or \"pip install .[opensimplex]\"") + raise ImportError(err) from None + + noise_array = np.empty((rows, columns), dtype=np.float32) + for y in range(0, rows): + for x in range(0, columns): + value = simplex.noise4(x / hillsize, y / hillsize, 0.0, 0.0) + color = int((value + 1) * 128) + noise_array[y, x] = color + + grid = GridObject() + + grid.path = '' + grid.z = noise_array + grid.rows = rows + grid.columns = columns + grid.shape = grid.z.shape + grid.cellsize = cellsize + + return grid + + +def gen_random_bool(rows: int = 32, columns: int = 32, cellsize: float = 10.0 + ) -> 'GridObject': + """Generate a GridObject instance that contains only randomly generated + Boolean values. + + Parameters + ---------- + rows : int, optional + Number of rows. Defaults to 32. + columns : int, optional + Number of columns. Defaults to 32. + cellsize : float, optional + Size of each cell in the grid. Defaults to 10.0. + + Returns + ------- + GridObject + An instance of GridObject with randomly generated Boolean values. + """ + bool_array = np.empty((rows, columns), dtype=np.float32) + + for y in range(0, rows): + for x in range(0, columns): + bool_array[x][y] = random.choice([0, 1]) + + grid = GridObject() + + grid.path = '' + grid.z = bool_array + grid.rows = rows + grid.columns = columns + grid.shape = grid.z.shape + grid.cellsize = cellsize + + return grid def get_dem_names() -> list[str]: """Returns a list of provided example Digital Elevation Models (DEMs). Requires internet connection to download available names. - Returns: - list[str]: A list of strings, where each string is the name of a DEM. + Returns + ------- + list[str] + A list of strings, where each string is the name of a DEM. """ with urlopen(DEM_NAMES) as dem_names: dem_names = dem_names.read().decode() @@ -40,16 +158,20 @@ def get_dem_names() -> list[str]: def load_dem(dem: str, cache: bool = True) -> GridObject: - """Downloads DEM from TopoToolbox/DEMs repository. - Find possible names by using 'get_dem_names()' - - Args: - dem (str): Name of dem about to be downloaded - cache (bool, optional): If true the dem will be cached. - Defaults to True. - - Returns: - GridObject: A GridObject generated from the downloaded dem. + """Downloads a DEM from the TopoToolbox/DEMs repository. + Find possible names by using 'get_dem_names()'. + + Parameters + ---------- + dem : str + Name of the DEM to be downloaded. + cache : bool, optional + If true, the DEM will be cached. Defaults to True. + + Returns + ------- + GridObject + A GridObject generated from the downloaded DEM. """ if dem not in get_dem_names(): err = ("Selected DEM has to be selected from the provided examples." + @@ -68,7 +190,7 @@ def load_dem(dem: str, cache: bool = True) -> GridObject: else: full_path = url - dem = GridObject(full_path) + dem = read_tif(full_path) return dem @@ -76,8 +198,10 @@ def load_dem(dem: str, cache: bool = True) -> GridObject: def get_save_location() -> str: """Generates filepath to file saved in cache. - Returns: - str: filepath to file saved in cache. + Returns + ------- + str + Filepath to file saved in cache. """ system = sys.platform @@ -100,13 +224,15 @@ def get_save_location() -> str: def clear_cache(filename: str = None) -> None: - """Deletes the cache directory and it's contents. Can also delete a single + """Deletes the cache directory and its contents. Can also delete a single file when using the argument filename. To get the contents of your cache, - use 'get_cache_contents()' + use 'get_cache_contents()'. - Args: - filename (str, optional): Add a filename if only one specific file is - to be deleted. Defaults to None. + Parameters + ---------- + filename : str, optional + Add a filename if only one specific file is to be deleted. + Defaults to None. """ path = get_save_location() @@ -126,8 +252,10 @@ def clear_cache(filename: str = None) -> None: def get_cache_contents() -> (list[str] | None): """Returns the contents of the cache directory. - Returns: - list[str]: List of all files in the topotoolbox cache. If cache does + Returns + ------- + list[str] + List of all files in the TopoToolbox cache. If cache does not exist, None is returned. """ path = get_save_location()