Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add anisotropic coefficient of variation #72

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ Contributing

If you would like to contribute to pytopotoolbox, refer to the :doc:`CONTRIBUTING`.

Wrapping libtopotoolbox functions
---------------------------------

Refer to :doc:`wrapping libtopotoolbox functions<wrapping>` for a quick guide on how to create new functions.

.. toctree::
:maxdepth: 1
:hidden:
Expand All @@ -39,6 +44,7 @@ If you would like to contribute to pytopotoolbox, refer to the :doc:`CONTRIBUTIN
API <api>
Contribution Guidelines <CONTRIBUTING>
Developer Documentation <dev>
Wrapping libtopotoolbox functions<wrapping>

Indices and Tables
------------------
Expand Down
230 changes: 230 additions & 0 deletions docs/wrapping.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to Wrap `libtopotoolbox` functions with the `pytopotoolbox`\n",
"\n",
"## What Needs to Be Done to Add a New Function\n",
"\n",
"You will need to modify content in the following directories and files:\n",
"\n",
"1. **src/topotoolbox/** [Refer to the section on wrapping pybind11 functions.](#wrapping-pybind11-functions)\n",
"\n",
"2. **src/topotoolbox/__init__.py** If you added a new file for a new class, you will need to add it here so it will be automatically imported when importing `topotoolbox`.\n",
"\n",
"3. **src/lib/** [Refer to the section on wrapping libtopotoolbox.](#creating-a-wrapper-for-libtopotoolbox-functions-using-pybind11)\n",
"\n",
"4. **CMakeLists.txt** If you added a new class, you will need to add a link using `target_link_libraries()`, `pybind_add_module()`, and the `install` section `install()`.\n",
"\n",
"5. **docs/api.rst** To include your function in the API documentation, add it here. Since we are using recursive autosummary, if your function is part of a class, it will automatically be added if the class is added to this file. If your function is not part of a class, you will need to add it manually.\n",
"\n",
"6. **tests/** Include tests for your function here.\n",
"\n",
"7. **examples/** If you want to provide an example as documentation for your function, create a new Jupyter notebook here. You can fill the file however you see fit, but make sure to include a section title for the file by making the first line your title and underlining it with `====`.\n",
"\n",
"8. **docs/examples.rst** If you added a new example, include it in the example gallery by adding a new line: `/_temp/name_of_your_example`\n",
"\n",
"9. **docs/conf.py** If you added a new example, you can also add a thumbnail for it here under the section `nbsphinx_thumbnails`.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Creating a wrapper for libtopotoolbox functions using pybind11\n",
"\n",
"We create a separate file for all wrappers of one class. The `src/lib/grip.cpp` will contain all wrappers for the GridObject class for example. Each file has to include the following code:\n",
"\n",
"```cpp\n",
"extern \"C\" {\n",
" #include <topotoolbox.h>\n",
"}\n",
"\n",
"#include <pybind11/pybind11.h>\n",
"#include <pybind11/numpy.h>\n",
"\n",
"namespace py = pybind11;\n",
"```\n",
"\n",
"Create a wrapper by creating a new function, we name them void wrap_func_name() to differentiate them from the libtopotoolbox functions. In this function you create pointers for your arrays and pass them to the function you are wrapping void func_name(). We are always editing our data in place, to all function return void.\n",
"\n",
"```cpp\n",
"void wrap_func_name(py::array_t<float> dem, std::tuple<ptrdiff_t,ptrdiff_t> dims){\n",
" float *dem_ptr = output.mutable_data();\n",
" std::array<ptrdiff_t, 2> dims_array = {std::get<0>(dims), std::get<1>(dims)};\n",
" ptrdiff_t *dims_ptr = dims_array.data();\n",
" func_name(dem_ptr, dims_ptr);\n",
"}\n",
"```\n",
"\n",
"At the end of the file we will include the function to the Pybind11 module. The module will be named after the class it's used for. For the `grid.cpp` file the module is named `_grid`.\n",
"\n",
"```cpp\n",
"PYBIND11_MODULE(_module_name_, m) {\n",
" m.def(\"function_name\", &wrap_function_name);\n",
"}\n",
"```\n",
"\n",
"When the topotoolbox package is beeing build these modules will be compiled and made available for the python wrappers."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Wrapping pybind11 functions\n",
"\n",
"Since the the pybind11 functions will only be available during the build process we always disable pylint errors when importing them into our python files. We do this so the code passes the pylint test we run in our .github/workflows and so that your IDE doesn't yell at you.\n",
"\n",
"```python\n",
"# pylint: disable=no-name-in-module\n",
"from . import _module_name # type: ignore\n",
"```\n",
"\n",
"When creating your python function you call the pybind11 wrapper like so:\n",
"\n",
"```python\n",
"_module_name.function_name()\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## What order arrays should be in\n",
"\n",
"Because Matlab is using column major order arrays, the libtopotoolbox originally used them as well. While thats not forced for new functions anymore, the shape (dims) we pass along with our matrix has to match the matrix. How the np.ndarray.shape is layed out, it works better just to use the column major order instead of having two different dims for each array (one to pass to C and one to use in python)\n",
"\n",
"The following example will showcase what this means in practice."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"# creating the same array, once in C and once in F order\n",
"array_c = np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], order='C')\n",
"array_f = np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], order='F')\n",
"dims = array_f.shape\n",
"\n",
"print(f\"Both arrays will look like this when looking at them:\\n{array_c}\")\n",
"\n",
"array_c = array_c.flatten(order='C')\n",
"array_f = array_f.flatten(order='F')\n",
"print(\"\\nBut when the arrays are passed to the C function, they will be\\n\"\n",
" \"flattened into on dimension like this:\"\n",
" f\"\\nC order: {array_c}\\nF order: {array_f}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As you can see, the Column major order (F), will result in each column being listed after one another instead of each row being listed after another (C).\n",
"\n",
"The first element of dims[2] (generated from the np.ndarray,shape) should be the size of the dimension that changes fastest as you scan through memory, e.g. rows for column-major/'F' order and cols for row-major/'C' order.\n",
"\n",
"In our example for F order you can see that 1, 4 and 7 are next to each other on memory. They are all in different rows. Therefor rows change faster than cols so the value denoting how many rows there are should be first in the dims[2]."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(dims)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In our example there are 4 rows and 3 columns. Since the rows are first in dims we should pass this array to the C code in F order.\n",
"\n",
"When looping trough the array we try to access the elements after another that are actually located next to each other in memory. Therefor the outer for loop should loop over dims[1] the inner over dims[0]."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for j in range(dims[1]):\n",
" for i in range(dims[0]):\n",
" location = j * dims[0] + i\n",
" print(array_f[location], end=', ')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To access neighboring cells in the same col we just need to add or subtract 1 from the index. For neighboring cells in same row we need to either add or subtract the length of one row (dims[0]) from the index. \n",
"\n",
"In our example the neighbors of 5 in memory are 2 and 8. The neigbors on the same row are 4 and 6."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(f\"array[5] = {array_f[5]}\")\n",
"print(f\"above = {array_f[5-1]}\")\n",
"print(f\"below = {array_f[5+1]}\")\n",
"print(f\"left = {array_f[5-dims[0]]}\")\n",
"print(f\"right = {array_f[5+dims[0]]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Alternative way to loop through the array:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for index in range(dims[0] * dims[1]):\n",
" col = index // dims[0] \n",
" row = index % dims[0] \n",
" print(f\"i: {index}, row: {row}, col: {col}, array[i] = {array_f[index]}\")"
]
}
],
"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
}
22 changes: 22 additions & 0 deletions src/lib/grid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,27 @@ void wrap_gradient8(
gradient8(output_ptr, dem_ptr, cellsize, use_mp, dims_ptr);
}


// wrap_avc:
// Parameters:
// output: A NumPy array which will store the resulting gradient of each cell.
// dem: A NumPy array containing the digital elevation model.
// use_mp: A int which will decide if OpenMP will be used.
// dims: A tuple containing the number of rows and columns.
//
// This function requires the arrays to be in 'C' order.

void wrap_acv(
py::array_t<float> output, py::array_t<float> dem,
int use_mp, std::tuple<ptrdiff_t,ptrdiff_t> dims){

float *output_ptr = output.mutable_data();
float *dem_ptr = dem.mutable_data();

std::array<ptrdiff_t, 2> dims_array = {std::get<0>(dims), std::get<1>(dims)};
ptrdiff_t *dims_ptr = dims_array.data();
acv(output_ptr, dem_ptr, use_mp, dims_ptr);
}
// Make wrap_funcname() function available as grid_funcname() to be used by
// by functions in the pytopotoolbox package

Expand All @@ -235,4 +256,5 @@ PYBIND11_MODULE(_grid, m) {
m.def("flow_routing_d8_carve", &wrap_flow_routing_d8_carve);
m.def("flow_routing_targets", &wrap_flow_routing_targets);
m.def("gradient8", &wrap_gradient8);
m.def("acv", &wrap_acv);
}
33 changes: 33 additions & 0 deletions src/topotoolbox/grid_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,39 @@ def gradient8(self, unit: str = 'tangent', multiprocessing: bool = True):

return result

def acv(self, multiprocessing: bool = True) -> 'GridObject':
"""
The anisotropic coefficient of variation (ACV) describes the general
geometry of the local land surface and can be used to distinguish elongated
from oval land forms.

Parameters
----------
multiprocessing : bool, optional
If True, use multiprocessing for computation. Default is True

Returns
-------
GridObject
GridObject containing the calculated anisotropic coefficient
of variation.
"""

if multiprocessing:
use_mp = 1
else:
use_mp = 0

dem = self.z.astype(np.float32, order='F')
output = np.zeros_like(dem)

_grid.acv(output, dem, use_mp, self.shape)

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

return result

def _gwdt_computecosts(self) -> np.ndarray:
"""
Compute the cost array used in the gradient-weighted distance
Expand Down
Loading