Skip to content

Commit

Permalink
Adds Graphflood binding (#57)
Browse files Browse the repository at this point in the history
* Start to add graphflood

* trying to pull request

* update that branch to match newer interface (not ready to merge)

* Update to latest upstream libttb and pytopotools

* reformat ipynb I ran

* Comply to flake8 format

* sort type conflict

* Use version 2024-W40 of libtopotoolbox

---------

Co-authored-by: William Kearney <[email protected]>
  • Loading branch information
bgailleton and wkearn authored Sep 30, 2024
1 parent 996ec57 commit 58e2d42
Show file tree
Hide file tree
Showing 32 changed files with 312 additions and 3 deletions.
Empty file modified .github/workflows/ci.yaml
100644 → 100755
Empty file.
Empty file modified .github/workflows/docs.yaml
100644 → 100755
Empty file.
Empty file modified .gitignore
100644 → 100755
Empty file.
8 changes: 5 additions & 3 deletions CMakeLists.txt
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ include(FetchContent)
FetchContent_Declare(
topotoolbox
GIT_REPOSITORY https://github.com/TopoToolbox/libtopotoolbox.git
#GIT_TAG main
GIT_TAG 2024-W34
GIT_TAG 2024-W40
)
FetchContent_MakeAvailable(topotoolbox)

Expand All @@ -23,4 +22,7 @@ target_link_libraries(_grid PRIVATE topotoolbox)
pybind11_add_module(_flow src/lib/flow.cpp)
target_link_libraries(_flow PRIVATE topotoolbox)

install(TARGETS _grid _flow LIBRARY DESTINATION topotoolbox)
pybind11_add_module(_graphflood src/lib/graphflood.cpp)
target_link_libraries(_graphflood PRIVATE topotoolbox)

install(TARGETS _grid _flow _graphflood LIBRARY DESTINATION topotoolbox)
Empty file modified CONTRIBUTING.md
100644 → 100755
Empty file.
Empty file modified LICENSE
100644 → 100755
Empty file.
Empty file modified LICENSES_bundled
100644 → 100755
Empty file.
Empty file modified README.md
100644 → 100755
Empty file.
Empty file modified docs/Makefile
100644 → 100755
Empty file.
Empty file modified docs/README.md
100644 → 100755
Empty file.
Empty file modified docs/_static/thumbnails/placeholder.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified docs/api.rst
100644 → 100755
Empty file.
Empty file modified docs/conf.py
100644 → 100755
Empty file.
Empty file modified docs/examples.rst
100644 → 100755
Empty file.
Empty file modified docs/index.rst
100644 → 100755
Empty file.
Empty file modified docs/installing.rst
100644 → 100755
Empty file.
Empty file modified docs/logo.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified docs/make.bat
100644 → 100755
Empty file.
Empty file modified docs/requirements.txt
100644 → 100755
Empty file.
Empty file modified docs/tutorial.ipynb
100644 → 100755
Empty file.
Empty file modified pyproject.toml
100644 → 100755
Empty file.
Empty file modified src/lib/flow.cpp
100644 → 100755
Empty file.
171 changes: 171 additions & 0 deletions src/lib/graphflood.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// This file contains bindings for the fillsinks function using PyBind11.

extern "C" {
#include <topotoolbox.h>
}

#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <stdbool.h>
#include <stdio.h>
#include <iostream>

namespace py = pybind11;


/*
Runs the full graphflood's algorithm as described in Gailleton et al., 2024
Z: 1D numpy array (n nodes) of topography (type np.float32)
hw: 1D numpy array (n nodes) of flow depth type np.float32)
BCs: 1D numpy array (n nodes) of boundary codes (type np.uint8)
Precipitations: 1D numpy array (n nodes) of precipitation rates in m.s-1 (type np.float32)
manning: 1D numpy array (n nodes) of friction coefficient (type np.float32)
dim: [nrows,ncolumns] for row major (e.g. python) or [ncolumns, nrows] for column major. Numpy array as np.uint64.
dt: time step (s ~ although this is not simulated time as we make the steady low assumption)
dx: spatial step (m)
D8: true to include diagonal paths
N_iterations: how many iterations to run
B.G. (last modification: 08/2024)
*/
void wrap_graphflood_full(
py::array_t<GF_FLOAT> Z,
py::array_t<GF_FLOAT> hw,
py::array_t<uint8_t> BCs,
py::array_t<GF_FLOAT> Precipitations,
py::array_t<GF_FLOAT> manning,
py::array_t<GF_UINT> dim,
GF_FLOAT dt,
GF_FLOAT dx,
bool SFD,
bool D8,
GF_UINT N_iterations,
GF_FLOAT step
){

// numpy arrays to pointers
GF_FLOAT* Z_ptr = Z.mutable_data() ;
GF_FLOAT* hw_ptr = hw.mutable_data() ;
uint8_t* BCs_ptr = BCs.mutable_data() ;
GF_FLOAT* Precipitations_ptr = Precipitations.mutable_data() ;
GF_FLOAT* manning_ptr = manning.mutable_data() ;
GF_UINT* dim_ptr = dim.mutable_data() ;

// calling the C function
graphflood_full(Z_ptr, hw_ptr, BCs_ptr, Precipitations_ptr, manning_ptr, dim_ptr, dt, dx, SFD, D8, N_iterations, step);
}


/*
Computes a single flow graph compatible with the boundary conditions system of graphflood
topo: 1D numpy array (n nodes) of topography (type np.float32)
Sreceivers: 1D numpy array (n nodes), flat index of steepest receiver (type np.uint64)
distToReceivers: 1D numpy array (n nodes), distance to steepest receiver (type np.float32)
Sdonors: 1D numpy array (n nodes * 4 or 8), flat index of donors (type np.uint64)
NSdonors: 1D numpy array (n nodes ), flat index of number of steepest donors (type np.uint8)
Stack: Topologically ordered nodes from downstream to upstream (stakc in Braun and Willett 2013)
BCs: 1D numpy array (n nodes) of boundary codes (type np.uint8)
dim: [nrows,ncolumns] for row major (e.g. python) or [ncolumns, nrows] for column major. Numpy array as np.uint64.
dx: spatial step (m)
D8: true to include diagonal paths
PF: true to also fill the topography using priority flood (Barnes, 2014)
B.G. (last modification: 08/2024)
*/
void wrap_compute_sfgraph(
py::array_t<GF_FLOAT> topo,
py::array_t<GF_UINT> Sreceivers,
py::array_t<GF_FLOAT> distToReceivers,
py::array_t<GF_UINT> Sdonors,
py::array_t<uint8_t> NSdonors,
py::array_t<GF_UINT> Stack,
py::array_t<uint8_t> BCs,
py::array_t<GF_UINT> dim,
GF_FLOAT dx,
bool D8,
bool PF,
GF_FLOAT step
){

GF_FLOAT *topo_ptr = topo.mutable_data() ;
GF_UINT *Sreceivers_ptr = Sreceivers.mutable_data() ;
GF_FLOAT *distToReceivers_ptr = distToReceivers.mutable_data() ;
GF_UINT *Sdonors_ptr = Sdonors.mutable_data() ;
uint8_t *NSdonors_ptr = NSdonors.mutable_data() ;
GF_UINT *Stack_ptr = Stack.mutable_data() ;
uint8_t *BCs_ptr = BCs.mutable_data() ;
GF_UINT *dim_ptr = dim.mutable_data() ;

if(PF)
compute_sfgraph_priority_flood(topo_ptr, Sreceivers_ptr, distToReceivers_ptr, Sdonors_ptr, NSdonors_ptr, Stack_ptr, BCs_ptr, dim_ptr, dx, D8, step);
else
compute_sfgraph(topo_ptr, Sreceivers_ptr, distToReceivers_ptr, Sdonors_ptr, NSdonors_ptr, Stack_ptr, BCs_ptr, dim_ptr, dx, D8);

}

void wrap_compute_drainage_area_single_flow(
py::array_t<GF_FLOAT> output,
py::array_t<GF_UINT> Sreceivers,
py::array_t<GF_UINT> Stack,
py::array_t<GF_UINT> dim,
GF_FLOAT dx
){

GF_FLOAT* output_ptr = output.mutable_data();
GF_UINT* Sreceivers_ptr = Sreceivers.mutable_data();
GF_UINT* Stack_ptr = Stack.mutable_data();
GF_UINT* dim_ptr = dim.mutable_data();


compute_drainage_area_single_flow(output_ptr, Sreceivers_ptr, Stack_ptr, dim_ptr, dx);
}

void wrap_compute_priority_flood_plus_stack(
py::array_t<GF_FLOAT> topo,
py::array_t<GF_UINT> Stack,
py::array_t<uint8_t> BCs,
py::array_t<GF_UINT> dim,
bool D8 ,
GF_FLOAT step
){

GF_FLOAT* topo_ptr = topo.mutable_data();
GF_UINT* Stack_ptr = Stack.mutable_data();
uint8_t* BCs_ptr = BCs.mutable_data();
GF_UINT* dim_ptr = dim.mutable_data();

// First priority flooding and calculating stack
compute_priority_flood_plus_topological_ordering(topo_ptr, Stack_ptr, BCs_ptr, dim_ptr, D8,step);
}

void wrap_compute_priority_flood(
py::array_t<GF_FLOAT> topo,
py::array_t<uint8_t> BCs,
py::array_t<GF_UINT> dim,
bool D8 ,
GF_FLOAT step
){

GF_FLOAT* topo_ptr = topo.mutable_data();
uint8_t* BCs_ptr = BCs.mutable_data();
GF_UINT* dim_ptr = dim.mutable_data();

// First priority flooding and calculating stack
compute_priority_flood(topo_ptr, BCs_ptr, dim_ptr, D8,step);
}


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

PYBIND11_MODULE(_graphflood, m) {
m.def("graphflood_run_full", &wrap_graphflood_full);
m.def("graphflood_sfgraph", &wrap_compute_sfgraph);
m.def("compute_priority_flood_plus_topological_ordering", &wrap_compute_priority_flood_plus_stack);
m.def("compute_priority_flood", &wrap_compute_priority_flood);
m.def("compute_drainage_area_single_flow", &wrap_compute_drainage_area_single_flow);


}
Empty file modified src/lib/grid.cpp
100644 → 100755
Empty file.
1 change: 1 addition & 0 deletions src/topotoolbox/__init__.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .grid_object import GridObject # noqa
from .flow_object import FlowObject # noqa
from .utils import * # noqa
from .graphflood import * # noqa
135 changes: 135 additions & 0 deletions src/topotoolbox/graphflood.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
Basic interface to libtopotoolbox implementation of graphflood
"""

from .grid_object import GridObject
import numpy as np
from ._graphflood import ( # type: ignore
graphflood_run_full,
graphflood_sfgraph,
compute_priority_flood_plus_topological_ordering,
compute_priority_flood,
compute_drainage_area_single_flow
)
from copy import deepcopy

# exposing function as string dictionary
funcdict = {
"run_full": graphflood_run_full,
"sfgraph": graphflood_sfgraph,
"priority_flood_TO": compute_priority_flood_plus_topological_ordering,
"priority_flood": compute_priority_flood,
"drainage_area_single_flow": compute_drainage_area_single_flow,
}

__all__ = ["run_graphflood"]


def run_graphflood(
grid: GridObject,
initial_hw=None,
BCs=None,
dt=1e-3,
P=10 * 1e-3 / 3600,
manning=0.033,
SFD=False,
D8=True,
N_iterations=100,
):

# Preparing the arguments
ny = grid.rows
nx = grid.columns
dx = grid.cellsize
dim = np.array([ny, nx], dtype=np.uint64)

# Order C for vectorised topography
Z = grid.z.ravel(order="C")

# Ingesting the flow depth
if initial_hw is None:
hw = np.zeros_like(Z)
else:
if initial_hw.shape != grid.z.shape:
raise RuntimeError(
"""Feeding the model with initial flow depth
requires a 2D numpy array or a GridObject of
the same dimension of the topographic grid"""
)

if isinstance(grid, GridObject):
hw = initial_hw.z.ravel(order="C")
else:
hw = initial_hw.ravel(order="C")

# Ingesting boundary condition
if BCs is None:
tBCs = np.ones((grid.rows, grid.columns), dtype=np.uint8)
tBCs[[0, -1], :] = 3
tBCs[:, [0, -1]] = 3
tBCs = tBCs.ravel(order="C")
else:
if BCs.shape != grid.shape:
raise RuntimeError(
"""Feeding the model with boundary conditions requires
a 2D numpy array or a GridObject of the same dimension
of the topographic grid"""
)

if isinstance(BCs, GridObject):
tBCs = BCs.z.ravel(order="C").astype(np.uint8)
else:
tBCs = BCs.ravel(order="C").astype(np.uint8)

# Ingesting Precipitations
if isinstance(P, np.ndarray):
if P.shape != grid.shape:
raise RuntimeError(
"""Feeding the model with precipitations requires a
2D numpy array or a GridObject of the same
dimension of the topographic grid"""
)
Precipitations = P.ravel(order="C")
elif isinstance(P, GridObject):
if P.shape != grid.shape:
raise RuntimeError(
"""Feeding the model with precipitations requires a
2D numpy array or a GridObject of the same dimension
of the topographic grid"""
)
Precipitations = P.z.ravel(order="C")
else:
# in case precipitation is a scalar
Precipitations = np.full_like(Z, P)

# Ingesting manning
if isinstance(manning, np.ndarray):
if manning.shape != grid.shape:
raise RuntimeError(
"""Feeding the model with precipitations requires a
2D numpy array or a GridObject of the same dimension
of the topographic grid"""
)
manning = manning.ravel(order="C")
elif isinstance(manning, GridObject):
if manning.shape != grid.shape:
raise RuntimeError(
"""Feeding the model with precipitations requires a
2D numpy array or a GridObject of the same dimension
of the topographic grid"""
)
manning = manning.z.ravel(order="C")
else:
# in case precipitation is a scalar
manning = np.full_like(Z, manning)

graphflood_run_full(
Z, hw, tBCs, Precipitations, manning,
dim, dt, dx, SFD, D8, N_iterations, 1e-3
)
res = deepcopy(grid)

res.z = hw.reshape(grid.shape)

return res
Empty file modified src/topotoolbox/grid_object.py
100644 → 100755
Empty file.
Empty file modified src/topotoolbox/utils.py
100644 → 100755
Empty file.
Empty file modified tests/README.md
100644 → 100755
Empty file.
Empty file modified tests/__init__.py
100644 → 100755
Empty file.
Empty file modified tests/test_grid_object.py
100644 → 100755
Empty file.
Empty file modified tests/test_utils.py
100644 → 100755
Empty file.

0 comments on commit 58e2d42

Please sign in to comment.