Skip to content

Commit

Permalink
Add Excesstopography (#54)
Browse files Browse the repository at this point in the history
* Setup excesstopography_fsm2d

* Add excesstopography example file

* Add arguments to excesstopography

* Complete excesstopography

* Add excesstopo example

* Fix type hinting and catch exceptions

* Add basic first test
  • Loading branch information
Teschl authored Jul 31, 2024
1 parent 5e4cbf8 commit bf94d11
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 10 deletions.
1 change: 1 addition & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Examples
========

.. nbgallery::
/_temp/excesstopography.ipynb
/_temp/plotting.ipynb
/_temp/test_genGrid
/_temp/test_GridObject
Expand Down
98 changes: 98 additions & 0 deletions examples/excesstopography.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Excesstopography\n",
"================\n",
"\n",
"This example will showcase how the excesstopography function can be used."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import topotoolbox as topo\n",
"dem = topo.gen_random()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"There are two methods that can be used to calculate the new GridObject. 'fsm2d' and 'fmm2d', where 'fsm2d' is the default since it requires less memory and is generally faster. This function needs a threshold matrix to calculate the excess topography. If no value/matrix is provided by the user, a default matrix filled with the value 0.2 is used."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"excess_fsm = dem.excesstopography(threshold=0.5)\n",
"topo.show(dem, excess_fsm)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"excess_fmm = dem.excesstopography(method='fmm2d')\n",
"topo.show(dem, excess_fmm)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If some sections od the GridObject should be differently than others, use another GridObject or np.ndarray to add custom value for the threshold slopes. Make sure that the shape of your threshold matches the one of your GridObject."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"# Generate custom matrix\n",
"custom_matrix = np.empty(dem.shape, order='F')\n",
"midpoint = dem.shape[0] // 2\n",
"custom_matrix[:midpoint, :] = 0.5\n",
"custom_matrix[midpoint:, :] = 0.2\n",
"\n",
"\n",
"excess_custom = dem.excesstopography(threshold=custom_matrix)\n",
"topo.show(dem, excess_custom)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
40 changes: 40 additions & 0 deletions src/lib/grid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,50 @@ void wrap_identifyflats(py::array_t<int32_t> output, py::array_t<float> dem, ptr
identifyflats(output_ptr, dem_ptr, nrows, ncols);
}

// wrap_excesstopography_fsm2d:
// Parameters:
// excess: A NumPy array to store the excess topography values computed by the FSM2D algorithm.
// dem: A NumPy array representing the digital elevation model.
// threshold_slopes: A NumPy array representing the slope thresholds for excess topography calculation.
// cellsize: The size of each cell in the DEM.
// nrows: Number of rows in the input DEM.
// ncols: Number of columns in the input DEM.

void wrap_excesstopography_fsm2d(py::array_t<float> excess, py::array_t<float> dem, py::array_t<float> threshold_slopes, float cellsize, ptrdiff_t nrows, ptrdiff_t ncols){
float *excess_ptr = excess.mutable_data();
float *dem_ptr = dem.mutable_data();
float *threshold_slopes_ptr = threshold_slopes.mutable_data();

excesstopography_fsm2d(excess_ptr, dem_ptr, threshold_slopes_ptr, cellsize, nrows, ncols);
}

// wrap_excesstopography_fmm2d:
// Parameters:
// excess: A NumPy array to store the excess topography values computed by the FMM2D algorithm.
// heap: A NumPy array representing the heap used in the FMM2D algorithm.
// back: A NumPy array representing the backtracking information used in the FMM2D algorithm.
// dem: A NumPy array representing the digital elevation model.
// threshold_slopes: A NumPy array representing the slope thresholds for excess topography calculation.
// cellsize: The size of each cell in the DEM.
// nrows: Number of rows in the input DEM.
// ncols: Number of columns in the input DEM.

void wrap_excesstopography_fmm2d(py::array_t<float> excess, py::array_t<ptrdiff_t> heap, py::array_t<ptrdiff_t> back, py::array_t<float> dem, py::array_t<float> threshold_slopes, float cellsize, ptrdiff_t nrows, ptrdiff_t ncols){
float *excess_ptr = excess.mutable_data();
ptrdiff_t *heap_ptr = heap.mutable_data();
ptrdiff_t *back_ptr = back.mutable_data();
float *dem_ptr = dem.mutable_data();
float *threshold_slopes_ptr = threshold_slopes.mutable_data();

excesstopography_fmm2d(excess_ptr, heap_ptr, back_ptr, dem_ptr, threshold_slopes_ptr, cellsize, nrows, ncols);
}

// Make wrap_funcname() function available as grid_funcname() to be used by
// by functions in the pytopotoolbox package

PYBIND11_MODULE(_grid, m) {
m.def("grid_fillsinks", &wrap_fillsinks);
m.def("grid_identifyflats", &wrap_identifyflats);
m.def("grid_excesstopography_fsm2d", &wrap_excesstopography_fsm2d);
m.def("grid_excesstopography_fmm2d", &wrap_excesstopography_fmm2d);
}
99 changes: 91 additions & 8 deletions src/topotoolbox/grid_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
import matplotlib.pyplot as plt

# pylint: disable=import-error
from ._grid import grid_fillsinks # type: ignore
from ._grid import grid_identifyflats # type: ignore
from ._grid import ( # type: ignore
grid_fillsinks,
grid_identifyflats,
grid_excesstopography_fsm2d,
grid_excesstopography_fmm2d
)

__all__ = ['GridObject']

Expand Down Expand Up @@ -38,7 +42,7 @@ def __init__(self) -> None:
self.transform = None
self.crs = None

def fillsinks(self):
def fillsinks(self) -> 'GridObject':
"""Fill sinks in the digital elevation model (DEM).
Returns
Expand Down Expand Up @@ -66,10 +70,11 @@ def identifyflats(
raw : bool, optional
If True, returns the raw output grid as np.ndarray.
Defaults to False.
output : list of str,
flat_neighbors = 0 optional
output : list of str, optional
List of strings indicating desired output types. Possible values
are 'sills', 'flats'. Defaults to ['sills', 'flats'].
are 'sills', 'flats'. Order of inputs in list are irrelevant,
first entry in output will always be sills.
Defaults to ['sills', 'flats'].
Returns
-------
Expand Down Expand Up @@ -110,7 +115,85 @@ def identifyflats(

return tuple(result)

def info(self):
def excesstopography(
self, threshold: "float | int | np.ndarray | GridObject" = 0.2,
method: str = 'fsm2d',) -> 'GridObject':
"""
Compute the two-dimensional excess topography using the specified method.
Parameters
----------
threshold : float, int, GridObject, or np.ndarray, optional
Threshold value or array to determine slope limits, by default 0.2.
If a float or int, the same threshold is applied to the entire DEM.
If a GridObject or np.ndarray, it must match the shape of the DEM.
method : str, optional
Method to compute the excess topography, by default 'fsm2d'.
Options are:
- 'fsm2d': Uses the fast sweeping method.
- 'fmm2d': Uses the fast marching method.
Returns
-------
GridObject
A new GridObject with the computed excess topography.
Raises
------
ValueError
If `method` is not one of ['fsm2d', 'fmm2d'].
If `threshold` is an np.ndarray and doesn't match the shape of the DEM.
TypeError
If `threshold` is not a float, int, GridObject, or np.ndarray.
"""

if method not in ['fsm2d', 'fmm2d']:
err = (f"Invalid method '{method}'. Supported methods are" +
" 'fsm2d' and 'fmm2d'.")
raise ValueError(err) from None

dem = self.z

if isinstance(threshold, (float, int)):
threshold_slopes = np.full(
dem.shape, threshold, order='F', dtype=np.float32)
elif isinstance(threshold, GridObject):
threshold_slopes = threshold.z
elif isinstance(threshold, np.ndarray):
threshold_slopes = threshold
else:
err = "Threshold must be a float, int, GridObject, or np.ndarray."
raise TypeError(err) from None

if not dem.shape == threshold_slopes.shape:
err = "Threshold array must have the same shape as the DEM."
raise ValueError(err) from None
if not threshold_slopes.flags['F_CONTIGUOUS']:
threshold_slopes = np.asfortranarray(threshold)
if not np.issubdtype(threshold_slopes.dtype, np.float32):
threshold_slopes = threshold_slopes.astype(np.float32)

excess = np.zeros_like(dem)
cellsize = self.cellsize
nrows, ncols = self.shape

if method == 'fsm2d':
grid_excesstopography_fsm2d(
excess, dem, threshold_slopes, cellsize, nrows, ncols)

elif method == 'fmm2d':
heap = np.zeros_like(dem, dtype=np.int64)
back = np.zeros_like(dem, dtype=np.int64)

grid_excesstopography_fmm2d(
excess, heap, back, dem, threshold_slopes, cellsize, nrows, ncols)

result = copy.copy(self)
result.z = excess

return result

def info(self) -> None:
"""Prints all variables of a GridObject.
"""
print(f"name: {self.name}")
Expand All @@ -122,7 +205,7 @@ def info(self):
print(f"transform: {self.transform}")
print(f"crs: {self.crs}")

def show(self, cmap='terrain'):
def show(self, cmap='terrain') -> None:
"""
Display the GridObject instance as an image using Matplotlib.
Expand Down
7 changes: 5 additions & 2 deletions src/topotoolbox/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ def read_tif(path: str) -> GridObject:


def gen_random(hillsize: int = 24, rows: int = 128, columns: int = 128,
cellsize: float = 10.0, seed: int = 3) -> 'GridObject':
cellsize: float = 10.0, seed: int = 3,
name: str = 'random grid') -> 'GridObject':
"""Generate a GridObject instance that is generated with OpenSimplex noise.
Parameters
Expand All @@ -156,6 +157,8 @@ def gen_random(hillsize: int = 24, rows: int = 128, columns: int = 128,
Size of each cell in the grid. Defaults to 10.0.
seed : int, optional
Seed for the terrain generation. Defaults to 3
name : str, optional
Name for the generated GridObject. Defaults to 'random grid'
Raises
------
Expand Down Expand Up @@ -191,7 +194,7 @@ def gen_random(hillsize: int = 24, rows: int = 128, columns: int = 128,
grid.columns = columns
grid.shape = grid.z.shape
grid.cellsize = cellsize

grid.name = name
return grid


Expand Down
3 changes: 3 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Test

To run the test locally, use: `pytest`
6 changes: 6 additions & 0 deletions tests/test_grid_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def tall_dem():


def test_fillsinks(square_dem, wide_dem, tall_dem):
# TODO: add more tests
for grid in [square_dem, wide_dem, tall_dem]:
# since grid is a fixture, it has to be assigned/called first
dem = grid
Expand Down Expand Up @@ -58,6 +59,7 @@ def test_fillsinks(square_dem, wide_dem, tall_dem):


def test_identifyflats(square_dem, wide_dem, tall_dem):
# TODO: add more tests
for dem in [square_dem, wide_dem, tall_dem]:
sills, flats = dem.identifyflats()

Expand All @@ -83,3 +85,7 @@ def test_identifyflats(square_dem, wide_dem, tall_dem):

if flats[i_neighbor, j_neighbor] < flats[i, j]:
assert flats[i, j] == 1.0

def test_excesstopography(square_dem):
# TODO: add more tests
assert square_dem.excesstopography(threshold='0.1') == TypeError()

0 comments on commit bf94d11

Please sign in to comment.