Skip to content

Commit 7ea237d

Browse files
authored
Merge pull request #74 from NikosMastrantonas/make_mct_rivers_mask
Make mct rivers mask
2 parents b326fbd + 86547aa commit 7ea237d

File tree

14 files changed

+381
-1
lines changed

14 files changed

+381
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Created by .ignore support plugin (hsz.mobi)
2+
.ipynb*
23
.idea
34
*.nc
45
**/__pycache__/
@@ -58,4 +59,4 @@ tests/data/gridding/meteo_out/**/*.kiwis
5859
.project
5960
.pydevproject
6061

61-
.ipynb_checkpoints/
62+
.ipynb_checkpoints/

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ NetCDF, PCRaster and TSS files.
7474

7575
* __[catchstats](#catchstats)__ calculates catchment statistics (mean, sum, std, min, max...) from NetCDF4 files given masks created with [cutmaps](#cutmaps).
7676

77+
* __[mctrivers](#mctrivers)__ creates a river mask for MCT diffusive river routing in LISFLOOD.
78+
> **Note**: PCRaster must be installed in the Conda environment.
79+
7780
The package contains convenient classes for reading/writing:
7881

7982
* PCRasterMap
@@ -925,6 +928,58 @@ The structure of the output depends on whether the input files include a tempora
925928
* If the input files DO NOT have a time dimension, the output has a single dimension: the catchment ID. It contains as many variables as the combinations of input variables and statistics. For instance, if the input variables are "elevation" and "gradient" and three statistics are required ("mean", "max", "min"), the output will contain 6 variables: "elevation_mean", "elevation_max", "elevation_min", "gradient_mean", "gradient_max" and "gradient_min".
926929
* If the input files DO have a time dimension, the output has two dimensions: the catchment ID and time. The number of variables follows the same structure explained in the previous point. For instance, if the input files are daily maps of precipitation (variable name "pr") and we calculate the mean and total precipitation over the catchment, the output will contain two dimensions ("ID", "time") and two variables ("pr_mean", "pr_sum").
927930

931+
932+
## mctrivers
933+
934+
This tool builds a mask of mild sloping rivers for use in LISFLOOD with MCT diffusive river routing. It takes LISFLOOD channels slope map (changrad.nc), the LDD (ldd.nc), the upstream drained area map (upArea.nc) and the catchment/domain mask (mask.nc), and outputs a bolean mask (chanmct.nc).
935+
Pixels where riverbed gradient < threshold (--slope) are added to the mask if their drainage area is large enough (--minuparea) and they also have at least --nloops consecutive downstream pixels that meet the same condition for slope (drainage area will be met as downstream the area increases).
936+
937+
### Usage
938+
939+
The tool requires the following mandatory input arguments:
940+
941+
- `-i`, `--changradfile`: LISFLOOD channels gradient map (changrad.nc)
942+
- `-l`, `--LDDfile`: LISFLOOD local drain direction file (ldd.nc)
943+
- `-u`, `--uparea`: LISFLOOD Uustream area file (upArea.nc)
944+
945+
The tool can take the following additional input arguments:
946+
947+
- `-m`, `--maskfile`: LISFLOOD mask or domain file (mask.nc; if not given, all domain is considered valid)
948+
- `-S`, `--slope`: Riverbed slope threshold to use MCT diffusive wave routing (default: 0.001)
949+
- `-N`, `--nloops`: Number of consecutive downstream grid cells that also need to comply with the slope requirement for including a grid cell in the MCT rivers mask (default: 5)
950+
- `-U`, `--minuparea`: Minimum upstream drainage area for a pixel to be included in the MCT rivers mask (uses the same units as in the -u file) (default: 0)
951+
- `-E`, `--coordsnames`: Coordinates names for lat, lon (in this order with space!) used in the netcdf files. The function checks for 3 commonly used names (x, lon, rlon for longitudes, and y, lat, rlat for latitudes). Therefere, it is recommended to keep the default value.
952+
953+
The tool generates the following outputs (when called from command line as main script):
954+
955+
- `-O`, `--outputfilename`: Output file containing the rivers mask where LISFLOOD can use the MCT diffusive wave routing (default: chanmct.nc)
956+
957+
It can be used either from command line, or also as a python function. Below follow some examples:
958+
959+
Example of command that will generate an MCT rivers mask with pixels where riverbed slope < 0.001 (same as default), drainage area > 500 kms and at least 5 (same as default) downstream pixels meet the same two conditions, considering the units of the upArea.nc file are given in kms:
960+
961+
```bash
962+
mctrivers -i changrad.nc -l ldd.nc -u upArea.nc -O chanmct.nc -U 500
963+
```
964+
965+
```python
966+
# no data are saved when called inside python
967+
from lisfloodutilities.mctrivers.mctrivers import mct_mask
968+
mct_mask_ds = mct_mask(channels_slope_file='changrad.nc', ldd_file='ldd.nc', uparea_file='upArea.nc', minuparea=500)
969+
```
970+
971+
Example of command that will generate an MCT rivers mask with pixels where riverbed slope < 0.0005, drainage area > 0 (same as default) kms and at least 3 downstream pixels meet the same two conditions. Also a mask (mask.nc) will be used, and the coords names in the nc files are "Lat1", "Lon1" for lat, lon respectively:
972+
973+
```bash
974+
mctrivers -i changrad.nc -l ldd.nc -u upArea.nc -O chanmct.nc -m mask.nc -E Lat1 Lon1 -S 0.0005 -N 3
975+
```
976+
977+
```python
978+
from lisfloodutilities.mctrivers.mctrivers import mct_mask
979+
mct_mask_ds = mct_mask(channels_slope_file='changrad.nc', ldd_file='ldd.nc', uparea_file='upArea.nc', mask_file='mask.nc', slp_threshold=0.0005, nloops=3, coords=["Lat1", "Lon1"])
980+
```
981+
982+
928983
## Using `lisfloodutilities` programmatically
929984

930985
You can use lisflood utilities in your python programs. As an example, the script below creates the mask map for a set of stations (stations.txt). The mask map is a boolean map with 1 and 0. 1 is used for all (and only) the pixels hydrologically connected to one of the stations. The resulting mask map is in pcraster format.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
3+
Copyright 2019-2020 European Union
4+
5+
Licensed under the EUPL, Version 1.2 or as soon they will be approved by the European Commission subsequent versions of the EUPL (the "Licence");
6+
7+
You may not use this work except in compliance with the Licence.
8+
You may obtain a copy of the Licence at:
9+
10+
https://joinup.ec.europa.eu/sites/default/files/inline-files/EUPL%20v1_2%20EN(1).txt
11+
12+
Unless required by applicable law or agreed to in writing, software distributed under the Licence is distributed on an "AS IS" basis,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the Licence for the specific language governing permissions and limitations under the Licence.
15+
16+
"""
17+
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# This is a script to create a mask with mild sloping river pixels.
2+
# It takes LISFLOOD channels slope map (changrad.nc), the LDD (ldd.nc) and mask of the catchment/domain.
3+
# Pixels where river slope < threshold are added to the mask, if drainage area is large enough.
4+
# A minimum number of consecutive mild sloping downstream pixels is required for a pixel to be added to the mask.
5+
# It requires PCRaster.
6+
#
7+
# Usage:
8+
# mctrivers.py -i changrad.nc -l ec_ldd.nc -m mask.nc -u upArea.nc -E y x -S 0.001 -N 5 -U 500 -O chanmct.nc
9+
10+
11+
import xarray as xr
12+
import pcraster as pcr
13+
import numpy as np
14+
15+
16+
def getarg():
17+
""" Get program arguments.
18+
19+
:return: args: namespace of program arguments
20+
"""
21+
import argparse
22+
parser = argparse.ArgumentParser()
23+
parser.add_argument('-i', '--changradfile', type=str, required=True,
24+
help='Changrad file with channels riverbed bed slope (chagrad.nc)')
25+
parser.add_argument('-l', '--LDDfile', type=str, required=True,
26+
help='LISFLOOD LDD file (ldd.nc)')
27+
parser.add_argument('-u', '--uparea', type=str, required=True,
28+
help='Upstream area file (upArea.nc)')
29+
parser.add_argument('-m', '--maskfile', type=str, required=False, default='',
30+
help='Mask or domain file (mask.nc)')
31+
parser.add_argument('-S', '--slope', type=float, required=False, default=0.001,
32+
help='Slope threshold to use MCT (default slp < 0.001)')
33+
parser.add_argument('-N', '--nloops', type=int, required=False, default=5, choices=range(0, 100),
34+
help='Number of consecutive downstream MCT gridcells to be an MCT cell (default = 5)')
35+
parser.add_argument('-U', '--minuparea', type=float, required=False, default=0,
36+
help='Minimum upstream area (same units as in the -u file) for including a cell in the final Muskingum mask (default 0)')
37+
parser.add_argument('-E', '--coordsnames', type=str, nargs='+', required=False, default="None",
38+
help='Coords names for lat, lon (in this order with space!) from the netcdf files used')
39+
parser.add_argument('-O', '--outputfilename', type=str, required=False, default="chanmct.nc",
40+
help='Output file (chanmct.nc)')
41+
args = parser.parse_args() # assign namespace to args
42+
return args
43+
44+
45+
def mct_mask(channels_slope_file, ldd_file, uparea_file, mask_file='',
46+
slp_threshold=0.001, nloops=5, minuparea=0, coords_names='None'):
47+
"""
48+
49+
Builds a mask of mild sloping rivers for use in LISFLOOD with MCT diffusive river routing. It takes LISFLOOD channels slope map (changrad.nc), the LDD (ldd.nc),
50+
the upstream drained area map (upArea.nc) and the catchment/domain mask (mask.nc), and outputs a bolean mask (chanmct.nc). Pixels where riverbed gradient < threshold
51+
(slp_threshold) are added to the mask if their drainage area is large enough (minuparea) and they also have at least nloops consecutive downstream pixels that meet
52+
the same condition for slope (drainage area will be met as downstream the area increases).
53+
54+
Usage:
55+
The tool requires the following input arguments:
56+
57+
channels_slope_file: LISFLOOD channels gradient map (changrad.nc)
58+
ldd_file: LISFLOOD local drain direction file (ldd.nc)
59+
uparea_file: LISFLOOD Upstream area file (upArea.nc)
60+
mask_file: a mask nc file; if not given (default) all cells are considered valid.
61+
slp_threshold: Riverbed slope threshold to use MCT diffusive wave routing (default: 0.001)
62+
nloops: Number of consecutive downstream grid cells that also need to comply with the slope requirement for including a grid cell in the MCT rivers mask (default: 5)
63+
minuparea: Minimum upstream drainage area for a pixel to be included in the MCT rivers mask (uses the same units as in the -u file) (default: 0)
64+
coords_names: Coordinates names for lat, lon (in this order as list) used in the the netcdf files (default: 'None'; checks for commonly used names ['x', 'lon', 'rlon'], similar for lat names)
65+
outputfile: Output file containing the rivers mask where LISFLOOD can use the MCT diffusive wave routing (default: chanmct.nc)
66+
67+
Example for generating an MCT rivers mask with pixels where riverbed slope < 0.001, drainage area > 500 kms and at least 5 downstream pixels meet the same
68+
two conditions, considering the units of the upArea.nc file are given in kms:
69+
70+
mct_mask(channels_slope_file='changrad.nc', ldd_file='ldd.nc', uparea_file='upArea.nc', mask_file='mask.nc',
71+
slp_threshold=0.001, nloops=5, minuparea=500, coords_names=['y' , 'x'])
72+
"""
73+
# ---------------- Read LDD (Note that for EFAS5 there is small shift of values for CH)
74+
LD = xr.open_dataset(ldd_file)
75+
76+
# ---------------- Auxiliary variables
77+
x_checks = ['lon', 'x', 'rlon']
78+
y_checks = ['lat', 'y', 'rlat']
79+
if coords_names == "None":
80+
x_proj = set(list(LD.coords)) & set(x_checks)
81+
y_proj = set(list(LD.coords)) & set(y_checks)
82+
83+
if len(x_proj)!=1 or len(y_proj)!=1:
84+
print('Input dataset coords names for lat/lon are not as expected.')
85+
print(f'The available coords are: {list(LD.coords)}')
86+
print(f'The checked names are {y_checks} and {x_checks} for lat and lon respectively.')
87+
print('Please use -E argument and give the coords names !with space in between! in order: lan lon')
88+
exit(1)
89+
90+
x_proj = list(x_proj)[0]
91+
y_proj = list(y_proj)[0]
92+
else:
93+
y_proj, x_proj = coords_names
94+
95+
# ---------------- Process channels slope netcdf
96+
# proprocess CH dataset for having correct format
97+
CH = xr.open_dataset(channels_slope_file)
98+
old_name = [i for i in list(CH.data_vars) if sorted(CH[i].dims)==sorted([x_proj, y_proj])]
99+
CH = CH.rename({old_name[0]: "changrad"}) # only 1 variable complies with the above check
100+
CH['changrad'] = CH['changrad'].transpose(y_proj, x_proj) # make sure dims order is as pcraster needs
101+
102+
# ---------------- Set clone map for pcraster
103+
# get number of rows and columns
104+
rows, cols = CH.sizes[y_proj], CH.sizes[x_proj]
105+
106+
# get coords of map corners
107+
x_all = CH.variables[x_proj]
108+
y_all = CH.variables[y_proj]
109+
x1 = x_all[0]
110+
x2 = x_all[-1]
111+
y1 = y_all[0]
112+
113+
# calc cell size
114+
cell_size = np.abs(x2 - x1) / (cols - 1)
115+
# calc coords
116+
x = x1 - cell_size / 2
117+
y = y1 + cell_size / 2
118+
119+
# set the clone map for pcraster
120+
pcr.setclone(rows, cols, cell_size, x, y)
121+
122+
# ---------------- Create rivers mask
123+
rivers_mask = (CH.changrad < slp_threshold)*1
124+
CH.close()
125+
126+
# convert the xarray to pcraster
127+
rivers_mask_pcr = pcr.numpy2pcr(pcr.Scalar, rivers_mask.values, 0)
128+
129+
# ---------------- Process LDD
130+
old_name = [i for i in list(LD.data_vars) if sorted(LD[i].dims)==sorted([x_proj, y_proj])]
131+
LD = LD.rename({old_name[0]: "ldd"})['ldd'] # only 1 variable complies with above if
132+
133+
# sometimes the masked value is flagged not with NaN (e.g., with cutmaps it was flagged with 0)
134+
# pcr.Ldd takes only integer values 1-9, so any other value needs to be masked
135+
LD = LD.fillna(-1) # fill NaN so it can be converted to integer with no issues
136+
LD = LD.astype('int')
137+
LD = LD.where((LD>0) & (LD<10)).fillna(-1)
138+
139+
# convert the xarray to pcraster
140+
LD = LD.transpose(y_proj, x_proj) # make sure dims order is as pcraster needs
141+
ldd_pcr = pcr.numpy2pcr(pcr.Ldd, LD.values, -1) # missing values in the np are flagged as -1
142+
143+
# repair the ldd; needed in case ldd is created from cutmaps, so outlet is not flagged with 5 (pit)
144+
ldd_pcr = pcr.lddrepair(ldd_pcr)
145+
146+
147+
# ---------------- Read upstream area
148+
UA = xr.open_dataset(uparea_file)
149+
old_name = [i for i in list(UA.data_vars) if sorted(UA[i].dims)==sorted([x_proj, y_proj])]
150+
UA = UA.rename({old_name[0]: "domain"})['domain'] # only 1 variable complies with the above if statement
151+
152+
# convert the xarray to pcraster
153+
UA = UA>=minuparea # check that the area is over the minimum
154+
minarea_bool_pcr = pcr.numpy2pcr(pcr.Boolean, UA.values, 0)
155+
UA.close()
156+
157+
# ---------------- Read domain/basin (mask) area
158+
try:
159+
MX = xr.open_dataset(mask_file)
160+
old_name = [i for i in list(MX.data_vars) if sorted(MX[i].dims)==sorted([x_proj, y_proj])]
161+
MX = MX.rename({old_name[0]: "domain"})['domain'] # only 1 variable complies with the above if statement
162+
except:
163+
print(f'The given mask path {mask_file} is not a valid path. All domain read from LDD file {ldd_file} is considered vaid.')
164+
MX = LD.copy(deep=True)
165+
166+
# use the exact same coords from channel slope file, just in case there are precision differences
167+
MX = MX.assign_coords({x_proj: x_all, y_proj: y_all})
168+
LD.close() # close ther LD file, after the check of mask availability
169+
170+
# ---------------- Loop on the basin pixels to find how many MCT pixels they have downstream
171+
# initiate a counter with 1 in cells that fit the slope criteria and 0 elsewhere
172+
sum_rivers_pcr = rivers_mask_pcr
173+
174+
# set the initial value of the 'downstream' pixels
175+
downstream_cells_pcr = rivers_mask_pcr
176+
177+
# Loop nloops times and use downstream function to find out if each cell has nloop MCT cells downstream
178+
# Downstream function gives the value in the downstream pixel in a map:
179+
# here it gives 1 if the downstream pixel is Muskingum, zero otherwise.
180+
# Note that for pits it always gives its own value so we need to mask out all pits as ann intermidiate step
181+
182+
# modify data, so that the most downstream point is masked out (otherwise the below loop gives for all points the same value)
183+
downstream_actual_mask_pcr = pcr.downstreamdist(ldd_pcr)
184+
downstream_actual_mask_pcr = pcr.ifthenelse(downstream_actual_mask_pcr == 0, pcr.boolean(0), pcr.boolean(1))
185+
downstream_actual_mask_pcr = pcr.scalar(downstream_actual_mask_pcr)
186+
187+
# The loop is used to count how many pixels are MCT downstream, as at each loop we move the values 1 pixel upstream
188+
# At the end of the loop, each element of the array has the number of downstream MCT pixels for that pixel
189+
for loops in range(0, nloops):
190+
# get the value on the downstream cell and put it in a mask
191+
downstream_cells_pcr = pcr.downstream(ldd_pcr, downstream_cells_pcr)
192+
downstream_cells_pcr = downstream_cells_pcr*downstream_actual_mask_pcr
193+
sum_rivers_pcr = sum_rivers_pcr + downstream_cells_pcr
194+
195+
# ---------------- Generate a new MCT rivers mask
196+
# Pixels with nloops downstream MCT pixels plus their self (nloops+1 in total) go to the MCT river mask
197+
mct_mask_pcr = pcr.ifthenelse(sum_rivers_pcr == nloops+1, pcr.boolean(1), pcr.boolean(0))
198+
199+
# Keep only the cells over the minimum area
200+
mct_mask_pcr = pcr.ifthenelse(minarea_bool_pcr, mct_mask_pcr, pcr.boolean(0))
201+
202+
# Use path function to include in the MCT mask all pixels downstream of an MCT pixel
203+
# path requires boolean 0-1. If there are NaNs then it gives wrong results!
204+
mct_mask_pcr = pcr.pcr2numpy(mct_mask_pcr, 0) # get the numpy from pcr
205+
mct_mask_pcr = pcr.numpy2pcr(pcr.Boolean, mct_mask_pcr, -1) # convert to Boolean with no NaN (-1 is not possible)
206+
mct_mask_pcr = pcr.path(ldd_pcr, mct_mask_pcr) # get the actual paths
207+
208+
# ---------------- Generate the output file
209+
mct_mask_np = pcr.pcr2numpy(mct_mask_pcr, 0)
210+
MCT = MX.fillna(0)*0+mct_mask_np
211+
MX.close()
212+
MCT.name = 'mct_mask'
213+
214+
# mask final data with the mask_file
215+
MCT = MCT.where(MX.notnull())
216+
217+
return MCT
218+
219+
220+
def main():
221+
'function for running from command line'
222+
# ---------------- Read settingss
223+
args = getarg()
224+
channels_slope_file_arg = args.changradfile
225+
ldd_file_arg = args.LDDfile
226+
uparea_file_arg = args.uparea
227+
mask_file_arg = args.maskfile
228+
slp_threshold_arg = args.slope
229+
nloops_arg = args.nloops
230+
minuparea_arg = args.minuparea
231+
coords_names_arg = args.coordsnames
232+
outputfile_arg = args.outputfilename
233+
234+
mct_final = mct_mask(channels_slope_file=channels_slope_file_arg, ldd_file=ldd_file_arg, uparea_file=uparea_file_arg, mask_file=mask_file_arg,
235+
slp_threshold=slp_threshold_arg, nloops=nloops_arg, minuparea=minuparea_arg, coords_names=coords_names_arg)
236+
# lisflood does not read NaNs so the data are saved as boolean 0-1, with -1 being flagged as NaN for python reading
237+
mct_final.to_netcdf(outputfile_arg, encoding={"mct_mask": {'_FillValue': -1, 'dtype': 'int8'}})
238+
239+
240+
if __name__ == "__main__":
241+
main()
Binary file not shown.
15.3 KB
Binary file not shown.
Binary file not shown.
21.9 KB
Binary file not shown.
Binary file not shown.
11.6 KB
Binary file not shown.
14.2 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)