Skip to content

Commit 0dbdeee

Browse files
authored
Merge pull request #26 from ecmwf/prep_for_v1
Prep for v1
2 parents ea6b4db + 7ad3def commit 0dbdeee

File tree

4 files changed

+83
-15
lines changed

4 files changed

+83
-15
lines changed

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ import earthkit.hydro as ekh
4949
The package contains different ways of constructing or loading a `RiverNetwork` object. A `RiverNetwork` object is a representation of a river network on a grid.
5050
It can be used to compute basic hydrological functions, such as propagating a scalar along the river network or extract a catchment from the river network.
5151

52+
### Mathematical Details
53+
Given a discretisation of a domain i.e. a set of points $\mathcal{D}=\{ (x_i, y_i)\}_{i=1}^N$, a river network is a directed acyclic graph $\mathcal{R}=(V,E)$ where the vertices $V \subseteq \mathcal{D}$. The out-degree of each vertex is at most 1 i.e. each point in the river network points to at most one downstream location.
54+
55+
For ease of notation, if an edge exists from $(x_i, y_i)$ to $(x_j, y_j)$, we write $i \rightarrow j$.
56+
5257
### Readers
5358

5459
```
@@ -79,31 +84,36 @@ Creates a `RiverNetwork` from a CaMa-Flood bin format of type "downxy" or "nextx
7984
```
8085
network.accuflux(field)
8186
```
82-
Calculates the total accumulated flux down a river network.
87+
Calculates the total accumulated flux down a river network.\
88+
$$v_i^{\prime}=v_i+\sum_{j \rightarrow i}~v_j^{\prime}$$
8389

8490
<img src="docs/images/accuflux.gif" width="200px" height="160px" />
8591

8692
```
8793
network.upstream(field)
8894
```
89-
Updates each node with the sum of its upstream nodes.
95+
Updates each node with the sum of its upstream nodes.\
96+
$$v_i^{\prime}=\sum_{j \rightarrow i}~v_j$$
9097

9198
```
9299
network.downstream(field)
93100
```
94-
Updates each node with its downstream node.
101+
Updates each node with its downstream node.\
102+
$$v_i^{\prime} = v_j, ~j ~ \text{s.t.} ~ i \rightarrow j$$
95103

96104
```
97105
network.catchment(field)
98106
```
99-
Finds the catchments (all upstream nodes of specified nodes, with overwriting).
107+
Finds the catchments (all upstream nodes of specified nodes, with overwriting).\
108+
$$v_i^{\prime} = v_j^{\prime} ~ \text{if} ~ v_j^{\prime} \neq 0 ~ \text{else} ~ v_i, ~j ~ \text{s.t.} ~ i \rightarrow j$$
100109

101110
<img src="docs/images/catchment.gif" width="200px" height="160px" />
102111

103112
```
104113
network.subcatchment(field)
105114
```
106-
Finds the subcatchments (all upstream nodes of specified nodes, without overwriting).
115+
Finds the subcatchments (all upstream nodes of specified nodes, without overwriting).\
116+
$$v_i^{\prime} = v_j^{\prime} ~ \text{if} ~ (v_j^{\prime} \neq 0 ~ \text{and} ~ v_j = 0) ~ \text{else} ~ v_i, ~j ~ \text{s.t.} ~ i \rightarrow j$$
107117

108118
<img src="docs/images/subcatchment.gif" width="200px" height="160px" />
109119

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ dynamic = ["version", "readme"]
2929

3030
dependencies = [
3131
"numpy",
32-
"xarray",
3332
"joblib"
3433
]
3534

@@ -39,6 +38,9 @@ dependencies = [
3938
issues = "https://github.com/ecmwf/earthkit-hydro/issues"
4039

4140
[project.optional-dependencies]
41+
netcdf = [
42+
"earthkit-data",
43+
]
4244
test = [
4345
"pytest",
4446
"pytest-cases",

src/earthkit/hydro/readers.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import numpy as np
2-
import xarray as xr
32
from .river_network import RiverNetwork
43
import joblib
54
from .caching import Cache
@@ -14,7 +13,7 @@ def load_river_network(
1413
cache_fname="{ekh_version}_{domain}_{version}.joblib",
1514
):
1615
"""
17-
Loads a river network from a specified.
16+
Loads a precomputed river network.
1817
A cache is used to store the river network file locally.
1918
2019
Parameters
@@ -44,6 +43,24 @@ def load_river_network(
4443
return network
4544

4645

46+
def load_saved_network(filepath):
47+
"""
48+
Loads a saved river network from file.
49+
50+
Parameters
51+
----------
52+
filepath : str
53+
Path to the saved river network joblib file.
54+
55+
Returns
56+
-------
57+
RiverNetwork
58+
The loaded river network object.
59+
"""
60+
network = joblib.load(filepath)
61+
return network
62+
63+
4764
def from_netcdf_d8(filename):
4865
"""
4966
Loads a river network from a NetCDF file using D8 direction encoding.
@@ -58,7 +75,13 @@ def from_netcdf_d8(filename):
5875
RiverNetwork
5976
The constructed river network object.
6077
"""
61-
data = xr.open_dataset(filename, mask_and_scale=False)["Band1"].values
78+
try:
79+
import earthkit.data as ekd
80+
except ModuleNotFoundError:
81+
raise ModuleNotFoundError(
82+
"earthkit-data is required for netcdf support.\nTo install it, run `pip install earthkit-data`"
83+
)
84+
data = ekd.from_source("file", filename).to_xarray(mask_and_scale=False)["Band1"].values
6285
return from_d8(data)
6386

6487

@@ -96,7 +119,7 @@ def from_d8(data):
96119
return create_network(upstream_indices, downstream_indices, missing_mask, shape)
97120

98121

99-
def from_netcdf_cama(filename, type="downxy"):
122+
def from_netcdf_cama(filename, type="nextxy"):
100123
"""
101124
Loads a river network from a CaMa-Flood style NetCDF file.
102125
@@ -117,10 +140,16 @@ def from_netcdf_cama(filename, type="downxy"):
117140
Exception
118141
If an unknown type is specified.
119142
"""
120-
data = xr.open_dataset(filename, mask_and_scale=False)
143+
try:
144+
import earthkit.data as ekd
145+
except ModuleNotFoundError:
146+
raise ModuleNotFoundError(
147+
"earthkit-data is required for netcdf support.\nTo install it, run `pip install earthkit-data`"
148+
)
149+
data = ekd.from_source("file", filename).to_xarray(mask_and_scale=False)
121150
if type == "downxy":
122151
dx, dy = data.downx.values, data.downy.values
123-
return from_cama_downxy(dx, -dy)
152+
return from_cama_downxy(dx, dy)
124153
elif type == "nextxy":
125154
x, y = data.nextx.values, data.nexty.values
126155
return from_cama_nextxy(x, y)
@@ -199,11 +228,11 @@ def from_cama_downxy(dx, dy):
199228
x_offsets = x_offsets[mask_upstream]
200229
y_offsets = y_offsets[mask_upstream]
201230

202-
upstream_indicies, downstream_indices = find_upstream_downstream_indices_from_offsets(
231+
upstream_indices, downstream_indices = find_upstream_downstream_indices_from_offsets(
203232
x_offsets, y_offsets, missing_mask, mask_upstream, shape
204233
)
205234

206-
return create_network(upstream_indicies, downstream_indices, missing_mask, shape)
235+
return create_network(upstream_indices, downstream_indices, missing_mask, shape)
207236

208237

209238
def from_cama_nextxy(x, y):

src/earthkit/hydro/river_network.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ class RiverNetwork:
191191
Groups of nodes sorted in topological order.
192192
"""
193193

194-
def __init__(self, nodes, downstream, mask, sinks=None, sources=None, topological_labels=None) -> None:
194+
def __init__(
195+
self, nodes, downstream, mask, sinks=None, sources=None, topological_labels=None, check_for_cycles=False
196+
) -> None:
195197
"""
196198
Initialises the RiverNetwork with nodes, downstream nodes, and a mask.
197199
@@ -203,6 +205,14 @@ def __init__(self, nodes, downstream, mask, sinks=None, sources=None, topologica
203205
Array of downstream node ids corresponding to each node.
204206
mask : numpy.ndarray
205207
A mask converting from the domain to the river network.
208+
sinks : numpy.ndarray, optional
209+
Array of sinks of the river network.
210+
sources : numpy.ndarray, optional
211+
Array of sources of the river network.
212+
topological_labels : numpy.ndarray, optional
213+
Array of precomputed topological distance labels.
214+
check_for_cycles : bool, optional
215+
Whether to check for cycles when instantiating the river network.
206216
"""
207217
self.nodes = nodes
208218
self.n_nodes = len(nodes)
@@ -212,6 +222,8 @@ def __init__(self, nodes, downstream, mask, sinks=None, sources=None, topologica
212222
sinks if sinks is not None else self.nodes[self.downstream_nodes == self.n_nodes]
213223
) # nodes with no downstreams
214224
self.sources = sources if sources is not None else self.get_sources() # nodes with no upstreams
225+
if check_for_cycles:
226+
self.check_for_cycles()
215227
self.topological_labels = (
216228
topological_labels if topological_labels is not None else self.compute_topological_labels()
217229
)
@@ -275,6 +287,19 @@ def get_sources(self):
275287
inlets = tmp_nodes[tmp_nodes != -1] # sources are nodes that are not downstream nodes
276288
return inlets
277289

290+
def check_for_cycles(self):
291+
"""
292+
Checks if the river network contains any cycles and raises an Exception if it does.
293+
"""
294+
nodes = self.downstream_nodes.copy()
295+
while True:
296+
if np.any(nodes == self.nodes):
297+
Exception("River Network contains a cycle.")
298+
elif np.all(nodes == self.n_nodes):
299+
break
300+
not_sinks = nodes != self.n_nodes
301+
nodes[not_sinks] = self.downstream_nodes[nodes[not_sinks]].copy()
302+
278303
def compute_topological_labels(self):
279304
"""
280305
Finds the topological distance labels for each node in the river network.
@@ -290,6 +315,8 @@ def compute_topological_labels(self):
290315
current_sum = 0 # sum of labels
291316
n = 1 # distance from source
292317
while current_sum > old_sum:
318+
if n > self.n_nodes:
319+
raise Exception("River Network contains a cycle.")
293320
old_sum = current_sum
294321
inlets = inlets[inlets != self.n_nodes] # subset to valid nodes
295322
labels[inlets] = n # update furthest distance from source

0 commit comments

Comments
 (0)