Automatic meshing#

This section aims to go into detail on how to generate a mesh automatically.

First, open a Python interface:

python3

Imports#

In [1]: import smash

In [2]: import numpy as np

In [3]: import matplotlib.pyplot as plt

Single gauge mesh#

The mesh on a single gauge is applied on the catchment of L'Ardèche (TODO add fig).

The minimum data to fill in are the coordinates of the outlet and the area in m².

In [4]: (x, y) = 823_629, 6_358_543

In [5]: area = 2264 * 1e6

Then we need to provide the path to the flow directions raster file. Here we will directly load the 1km France flow directions from the smash.load_dataset() method.

Warning

The flow directions file and the (x, y) coordinates must be in the same CRS. Here we use the Lambert93 cartesien projection (EPSG:2154).

In [6]: flwdir = smash.load_dataset("flwdir")

We can now generate the mesh using the smash.generate_mesh() method.

In [7]: mesh = smash.generate_mesh(
   ...:         path=flwdir,
   ...:         x=x,
   ...:         y=y,
   ...:         area=area,
   ...: )
   ...: 

Check output#

Once the mesh is generated, the user can visualize the output to confirm that the meshing has been done correctly.

The simpliest way to check if the meshing is correct (or at least, not completely wrong) is to check the modeled x, y and area compared to the observed data.

# GAUGE POS STORE (ROW, COL) INDICES
# ROW
In [8]: y_mod = mesh["ymax"] - mesh["gauge_pos"][0, 0] * mesh["dx"]

# COL
In [9]: x_mod = mesh["xmin"] + mesh["gauge_pos"][0, 1] * mesh["dx"]

In [10]: x_mod, x
Out[10]: (823000.0, 823629)

In [11]: y_mod, y
Out[11]: (6358000.0, 6358543)

In [12]: distance = np.sqrt((x - x_mod) ** 2 + (y - y_mod) ** 2)

In [13]: distance
Out[13]: 830.957279286968

The distance between the observed outlet and the modeled outlet is approximately 831 meters. Concerning the area.

In [14]: area_mod = mesh["area"][0]

In [15]: area_mod, area
Out[15]: (2257000000.0, 2264000000.0)

In [16]: rel_err = (area - area_mod) / area

In [17]: rel_err
Out[17]: 0.0030919010600706713

The relative error between observed area and modeled area is approximately 0.3%.

Then, we can visualize any map such as the flow distances.

In [18]: plt.imshow(mesh["flwdst"]);

In [19]: plt.colorbar(label="Flow distance (m)");

In [20]: plt.title("Single gauge - Flow distance");
../../_images/user_guide.in_depth.automatic_meshing.flwdst_single_gauge.png

Missmatching data#

It can sometimes happen that the meshing observed data (x, y, and area) is not consistent with the flow directions. We will assume in the following case that we have a shift of the catchment outlet real coordinates.

In [21]: x_off = x + 2_000
In [22]: mesh_off = smash.generate_mesh(
   ....:         path=flwdir,
   ....:         x=x_off,
   ....:         y=y,
   ....:         area=area,
   ....: )
   ....: 

In [23]: area_mod = mesh_off["area"][0]

In [24]: area_mod, area
Out[24]: (26000000.0, 2264000000.0)

In [25]: rel_err = (area - area_mod) / area

In [26]: rel_err
Out[26]: 0.9885159010600707

In [27]: plt.imshow(mesh_off["flwdst"]);

In [28]: plt.colorbar(label="Flow distance (m)");

In [29]: plt.title("Missmatch single gauge - Flow distance");
../../_images/user_guide.in_depth.automatic_meshing.missmatch_flwdst_single_gauge.png

As shown by the relative error on the areas (98%) and the flow distances, we did not generate the expected meshing for the catchment.

Nested multiple gauge mesh#

The mesh on nested multiple gauge is still applied on the catchment of L'Ardèche for 4 gauges (TODO add fig).

Instead of being float, x, y and area are lists of float.

In [38]: x = np.array([786875, 770778, 810350, 823714])

In [39]: y = np.array([6370217, 6373832, 6367508, 6358435])

In [40]: area = np.array([496, 103, 1958, 2264]) * 1e6

Note

x, y and area are NumPy arrays but could’ve been lists, tuples or sets (any list-like object) but working with NumPy arrays makes the operations easier.

Then call the smash.generate_mesh() method.

In [41]: mesh = smash.generate_mesh(
   ....:         path=flwdir,
   ....:         x=x,
   ....:         y=y,
   ....:         area=area,
   ....: )
   ....: 

Check output#

Same as the single gauge, we can confirm that the mesh has been correctly done by checking distances and areas.

In [42]: y_mod = mesh["ymax"] - mesh["gauge_pos"][:, 0] * mesh["dx"]

In [43]: x_mod = mesh["xmin"] + mesh["gauge_pos"][:, 1] * mesh["dx"]

In [44]: distance = np.sqrt((x - x_mod) ** 2 + (y - y_mod)** 2)

In [45]: rel_err = (area - mesh["area"]) / area

In [46]: distance
Out[46]: array([250.42763426, 795.93215791, 603.79135469, 836.07475742])

In [47]: rel_err
Out[47]: array([ 0.        , -0.03883495,  0.00153214,  0.0030919 ])

As well as visualize the flow distances map, which will be the same as the single gauge case because the flow distances are only calculated for the most downstream gauge in case of nested gauges.

In [48]: plt.imshow(mesh["flwdst"]);

In [49]: plt.colorbar(label="Flow distance (m)");

In [50]: plt.title("Nested multiple gauge - Flow distance");
../../_images/user_guide.in_depth.automatic_meshing.flwdst_multiple_gauge.png

Gauges location#

One way to visualize where the 4 gauges are located.

In [51]: canvas = np.zeros(shape=mesh["flwdir"].shape)

In [52]: canvas = np.where(mesh["active_cell"] == 0, np.nan, canvas)

In [53]: for pos in mesh["gauge_pos"]:
   ....:     canvas[tuple(pos)] = 1
   ....: 

In [54]: plt.imshow(canvas, cmap="Set1_r");

In [55]: plt.title("Nested multiple gauge - Gauges location");
../../_images/user_guide.in_depth.automatic_meshing.gauge_pos_multiple_gauge.png

Gauges code#

When working with a single gauge, it is not usefull to give a code for the gauge. When working with multiple gauge, especially in case of optimization, we need to know how to call the gauges. By default, if no code are given to the smash.generate_mesh() method, the codes are the following.

In [56]: mesh["code"]
Out[56]: array(['_c0', '_c1', '_c2', '_c3'], dtype='<U3')

The gauges code are always sorted in the same way than the gauges location.

The default codes are generally not enought explicit and the user can provide it’s own gauges code by directly change the mesh dictionary value or filling in the argument code in the smash.generate_mesh()

In [57]: code = np.array(["V5045030", "V5046610", "V5054010", "V5064010"])

In [58]: code
Out[58]: array(['V5045030', 'V5046610', 'V5054010', 'V5064010'], dtype='<U8')

In [59]: mesh["code"] = code.copy()

In [60]: mesh = smash.generate_mesh(
   ....:         path=flwdir,
   ....:         x=x,
   ....:         y=y,
   ....:         area=area,
   ....:         code=code,
   ....: )
   ....: 

In [61]: mesh["code"]
Out[61]: array(['V5045030', 'V5046610', 'V5054010', 'V5064010'], dtype='<U8')

Warning

When setting gauges code directly in the mesh dictionary, the value must be a NumPy array. Otherwise, similar to the arguments x, y and area, the codes can be any list-like object (NumPy array, list, tuple or set).

Non-nested multiple gauge mesh#

The mesh on non-nested multiple gauge is still applied on the catchment of L'Ardèche for 4 gauges and on the catchment of Le Gardon for 3 gauges (TODO add fig).

There is no difference in the way the method smash.generate_mesh() is called between nested and non-nested gauges.

So we fill in x, y and the area.

In [62]: x = np.array(
   ....:     [786875, 770778, 810350, 823714, 786351, 778264, 792628]
   ....: )
   ....: 

In [63]: y = np.array(
   ....:     [6370217, 6373832, 6367508, 6358435, 6336298, 6349858, 6324727]
   ....: )
   ....: 

In [64]: area = np.array(
   ....:     [496, 103, 1958, 2264, 315, 115, 1093]
   ....: ) * 1e6
   ....: 

Then call the smash.generate_mesh() method.

In [65]: mesh = smash.generate_mesh(
   ....:         path=flwdir,
   ....:         x=x,
   ....:         y=y,
   ....:         area=area,
   ....: )
   ....: 

In [66]: plt.imshow(mesh["flwdst"]);

In [67]: plt.colorbar(label="Flow distance (m)");

In [68]: plt.title("Non-nested multiple gauge - Flow distance");
../../_images/user_guide.in_depth.automatic_meshing.flwdst_non-nested_multiple_gauge.png

The mesh has been generated for two groups of catchments which are non-nested.

Note

The flow distances are always calculated on the most downstream gauge. In case of non-nested groups of catchments, the flow distance are calculated for each group on the most downstream gauge.

Finally, visualize the gauge positions for this mesh.

In [69]: canvas = np.zeros(shape=mesh["flwdir"].shape)

In [70]: canvas = np.where(mesh["active_cell"] == 0, np.nan, canvas)

In [71]: for pos in mesh["gauge_pos"]:
   ....:     canvas[tuple(pos)] = 1
   ....: 

In [72]: plt.imshow(canvas, cmap="Set1_r");

In [73]: plt.title("Non-nested multiple gauge - Gauges location");
../../_images/user_guide.in_depth.automatic_meshing.gauge_pos_non-nested_multiple_gauge.png