diff --git a/docs/api.rst b/docs/api.rst index a0cc5346..9eb27f7d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -44,12 +44,16 @@ Setup components SfincsModel.setup_river_outflow SfincsModel.setup_observation_points SfincsModel.setup_structures + SfincsModel.setup_drainage_structures SfincsModel.setup_waterlevel_forcing SfincsModel.setup_waterlevel_bnd_from_mask SfincsModel.setup_discharge_forcing SfincsModel.setup_discharge_forcing_from_grid SfincsModel.setup_precip_forcing SfincsModel.setup_precip_forcing_from_grid + SfincsModel.setup_pressure_forcing_from_grid + SfincsModel.setup_wind_forcing + SfincsModel.setup_wind_forcing_from_grid SfincsModel.setup_tiles Plot methods @@ -182,6 +186,8 @@ Input/Output methods utils.write_xyn utils.read_geoms utils.write_geoms + utils.read_drn + utils.write_drn utils.read_sfincs_map_results utils.read_sfincs_his_results diff --git a/docs/changelog.rst b/docs/changelog.rst index 1f36aad0..57249db2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,17 +8,24 @@ The format is based on `Keep a Changelog`_, and this project adheres to v1.0.1 (unreleased) =================== - Bugfix ------ - bugfix in `SfincsModel.write_forcing` to ensure all NetCDF files are written instead of only the first one. PR #86 - bugfix in `SfincsModel.read_config` & `SfincsInput.read` for relative paths in inp file. PR #88 - bugfix in `SfincsModel.setup_subgrid` to ensure that a 'big geotiff' will be written by default when 'write_dep_tif' or 'write_man_tif' are True - fix memory issues caused by rasterizing the model region and reprojecting before clipping of rasters. PR #94 +- bugfix in `Sfincs.read_forcing` when combining attributes from the locations stored in the gis folder with the actual forcing locations. PR #99 +- bugfix in `SfincsModel.setup_discharge_from_grid` when snapping based on upstream area in case a src points is outside of the uparea grid domain. PR #99 New ----------- -- `SfincsModel.setup_cn_infiltration_with_kr` to setup three layers related to the curve number (maximum and effective infiltration capacity; seff and smax) and recovery rate (kr) +- `SfincsModel.setup_cn_infiltration_with_kr` to setup three layers related to the curve number (maximum and effective infiltration capacity; seff and smax) and recovery rate (kr). PR#87 +- `SfincsModelsetup_drainage_structures` to setup drainage structures (pumps,culverts) from a geodataframe. PR#90 +- Added `SfincsModel.setup_wind_forcing`, `SfincsModel.setup_wind_forcing_from_grid` and `SfincsModel.setup_pressure_forcing_from_grid` methods to easily add wind and pressure forcing. PR #92 +- `SfincsModel.read_config` allows to use a template input file from a directory different than the model root. PR #102 +- Added the option to use landuse/landcover data combined with a reclass table to `SfincsModel.setup_constant_infiltration`. PR #103 +- Enabled to provide locations only (so no timeseries) for `SfincsModel.setup_waterlevel_forcing` and `SfincsModel.setup_discharge_forcing` PR #104 +- New optional buffer argument in `SfincsModel.setup_discharge_forcing` to select gauges around boundary only. PR #104 Changed ------- @@ -206,4 +213,4 @@ Documentation .. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/ -.. _Semantic Versioning: https://semver.org/spec/v2.0.0.html \ No newline at end of file +.. _Semantic Versioning: https://semver.org/spec/v2.0.0.html diff --git a/docs/user_guide/sfincs_model_setup.rst b/docs/user_guide/sfincs_model_setup.rst index f3c5023e..e5067c5b 100644 --- a/docs/user_guide/sfincs_model_setup.rst +++ b/docs/user_guide/sfincs_model_setup.rst @@ -36,7 +36,7 @@ For more information about each file, see the `SFINCS documentation 1: @@ -2802,42 +3133,35 @@ def write_vector( ## model configuration - def read_config(self, config_fn: str = "sfincs.inp", epsg: int = None) -> None: + def read_config(self, config_fn: str = None, epsg: int = None) -> None: """Parse config from SFINCS input file. - If in write-only mode the config is initialized with default settings. + If in write-only mode the config is initialized with default settings + unless a path to a template config file is provided. Parameters ---------- config_fn: str - Filename of config file, by default "sfincs.inp". - If in a different folder than the model root, the root is updated. + Filename of config file, by default None. epsg: int - EPSG code of the model CRS. Only used if missing in the SFINCS input file, by default None. + EPSG code of the model CRS. Only used if missing in the SFINCS input file, + by default None. """ inp = SfincsInput() # initialize with defaults - if self._read: # in read-only or append mode, try reading config_fn - if not isfile(config_fn) and not isabs(config_fn) and self._root: - # path relative to self.root + if config_fn is not None or self._read: + if config_fn is None: # read from default location + config_fn = self._config_fn + if not isabs(config_fn) and self._root: # read from model root config_fn = abspath(join(self.root, config_fn)) - elif isfile(config_fn) and abspath(dirname(config_fn)) != self._root: - # new root - mode = ( - "r+" - if self._write and self._read - else ("w" if self._write else "r") - ) - root = abspath(dirname(config_fn)) - self.logger.warning(f"updating the model root to: {root}") - self.set_root(root=root, mode=mode) - else: + if not isfile(config_fn): raise IOError(f"SFINCS input file not found {config_fn}") - # read config_fn + # read inp file inp.read(inp_fn=config_fn) # overwrite / initialize config attribute self._config = inp.to_dict() if epsg is not None and "epsg" not in self.config: - self.config.update(epsg=epsg) - self.update_grid_from_config() # update grid properties based on sfincs.inp + self.set_config("epsg", int(epsg)) + # update grid properties based on sfincs.inp + self.update_grid_from_config() def write_config(self, config_fn: str = "sfincs.inp"): """Write config to """ diff --git a/hydromt_sfincs/utils.py b/hydromt_sfincs/utils.py index 1f2491ed..86a87a19 100644 --- a/hydromt_sfincs/utils.py +++ b/hydromt_sfincs/utils.py @@ -40,6 +40,8 @@ "write_xyn", "read_geoms", "write_geoms", + "read_drn", + "write_drn", "gdf2linestring", "gdf2polygon", "linestring2gdf", @@ -315,7 +317,7 @@ def write_timeseries( data[:, 0] = (df.index - tref).total_seconds() # calculate required width for time column; hard coded single decimal precision # format for other columns is based on fmt`argument - w = int(np.floor(np.log10(data[-1, 0]))) + 3 + w = int(np.floor(np.log10(abs(data[-1, 0])))) + 3 fmt_lst = [f"%{w}.1f"] + [fmt for _ in range(df.columns.size)] fmt_out = " ".join(fmt_lst) with open(fn, "w") as f: @@ -623,6 +625,98 @@ def read_geoms(fn: Union[str, Path]) -> List[Dict]: return feats +def write_drn(fn: Union[str, Path], gdf_drainage: gpd.GeoDataFrame, fmt="%.4f") -> None: + """Write structure files from list of dictionaries. + + Parameters + ---------- + fn : str, Path + Path to structure file. + drainage : gpd.GeoDataFrame + Dataframe with drainage structure parameters and geometry. + fmt : str + Format for float values. + """ + + # expected columns for drainage structures + col_names = [ + "xsnk", + "ysnk", + "xsrc", + "ysrc", + "type", + "par1", + "par2", + "par3", + "par4", + "par5", + ] + + gdf = copy.deepcopy(gdf_drainage) + # get geometry linestring and convert to xsnk, ysnk, xsrc, ysrc + endpoints = gdf.boundary.explode().unstack() + gdf["xsnk"] = endpoints[0].x + gdf["ysnk"] = endpoints[0].y + gdf["xsrc"] = endpoints[1].x + gdf["ysrc"] = endpoints[1].y + gdf.drop(["geometry"], axis=1, inplace=True) + + # reorder columns based on col_names + gdf = gdf[col_names] + + # write to file + gdf.to_csv(fn, sep=" ", index=False, header=False, float_format=fmt) + + +def read_drn(fn: Union[str, Path], crs: int = None) -> gpd.GeoDataFrame: + """Read drainage structure files to geodataframe. + + Parameters + ---------- + fn : str, Path + Path to drainge structure file. + crs : int + EPSG code for coordinate reference system. + + Returns + ------- + gpd.GeoDataFrame + Dataframe with drainage structure parameters and geometry. + """ + + # expected columns for drainage structures + col_names = [ + "xsnk", + "ysnk", + "xsrc", + "ysrc", + "type", + "par1", + "par2", + "par3", + "par4", + "par5", + ] + + # read structure file + df = pd.read_csv(fn, sep="\\s+", names=col_names) + + # get geometry linestring + geom = [ + LineString([(xsnk, ysnk), (xsrc, ysrc)]) + for xsnk, ysnk, xsrc, ysrc in zip( + df["xsnk"], df["ysnk"], df["xsrc"], df["ysrc"] + ) + ] + df.drop(["xsnk", "ysnk", "xsrc", "ysrc"], axis=1, inplace=True) + + # convert to geodataframe + gdf = gpd.GeoDataFrame(df, geometry=geom) + if crs is not None: + gdf.set_crs(crs, inplace=True) + return gdf + + ## OUTPUT: sfincs_map.nc, sfincs_his.nc ## diff --git a/hydromt_sfincs/workflows/curvenumber.py b/hydromt_sfincs/workflows/curvenumber.py index e523bf85..910ede7c 100644 --- a/hydromt_sfincs/workflows/curvenumber.py +++ b/hydromt_sfincs/workflows/curvenumber.py @@ -62,10 +62,7 @@ def scs_recovery_determination(da_landuse, da_HSG, da_Ksat, df_map, da_mask_bloc # very high 100 - Inf da_kr = da_Ksat.raster.reproject_like(da_kr, method="average").load() da_kr = np.minimum(da_kr, 100) # not higher than 100 - da_kr = da_kr * 0.141732 - # from micrometers per second to inch/hr (constant) - da_kr = np.sqrt(da_kr) / 75 - # recovery in percentage of Smax per hour (Eq. 4.36) + da_kr = da_kr * 3.6 # from micrometers per second to mm/hr (constant) # Ensure no NaNs da_smax = da_smax.fillna(0) diff --git a/hydromt_sfincs/workflows/discharge.py b/hydromt_sfincs/workflows/discharge.py index b601da1b..3ad1730e 100644 --- a/hydromt_sfincs/workflows/discharge.py +++ b/hydromt_sfincs/workflows/discharge.py @@ -54,35 +54,35 @@ def snap_discharge( logger.debug( f"Snapping {discharge_name} points to best matching uparea cell within wdw (size={wdw})." ) + # find cells in window with smallest difference in uparea upa0 = xr.DataArray(gdf[uparea_name], dims=("index")) upa_dff = np.abs( ds_wdw[uparea_name].where(ds_wdw[uparea_name] > 0).load() - upa0 ) + i_wdw = upa_dff.fillna(np.inf).argmin("wdw") + # find valid cells based on error criteria upa_check = np.logical_or((upa_dff / upa0) <= rel_error, upa_dff <= abs_error) valid = np.logical_and(valid, upa_check) - # combine valid local cells with best matching windows cells if local cell invalid - # i_loc = int((1 + 2 * wdw) ** 2 / 2) # center cell - # i_wdw = upa_dff.argmin("wdw").where(~valid.isel(wdw=i_loc), i_loc).load() - # find best matching uparea cell in window - i_wdw = upa_dff.argmin("wdw").load() else: logger.debug( f"No {uparea_name} variable found in ds or gdf; " f"sampling {discharge_name} points from nearest grid cell." ) - # add distance (measured in cells) + # calculate distance to center cell (measured in cells) ar_wdw = np.abs(np.arange(-wdw, wdw + 1)) dist = np.hypot(**np.meshgrid(ar_wdw, ar_wdw)).ravel() ds_wdw["dist"] = xr.Variable( ("index", "wdw"), np.tile(dist, (ds_wdw["index"].size, 1)) ) + # find nearest valid cell in window i_wdw = ds_wdw["dist"].where(valid, np.inf).argmin("wdw").load() + # filter valid cells idx_valid = np.where(valid.isel(wdw=i_wdw).values)[0] if idx_valid.size < gdf.index.size: logger.warning( f"{idx_valid.size}/{gdf.index.size} {discharge_name} points successfully snapped." ) i_wdw = i_wdw.isel(index=idx_valid) + # return discharge at valid cells ds_out = ds_wdw.isel(wdw=i_wdw.load(), index=idx_valid) - - return ds_out # .reset_coords()[discharge_name] + return ds_out diff --git a/tests/data/local_data.yml b/tests/data/local_data.yml index b41119d3..a6bacee6 100644 --- a/tests/data/local_data.yml +++ b/tests/data/local_data.yml @@ -16,3 +16,8 @@ weir: data_type: GeoDataFrame driver: vector path: local_data/weir.geojson + +drainage: + data_type: GeoDataFrame + driver: vector + path: local_data/drainage.geojson diff --git a/tests/data/local_data/drainage.geojson b/tests/data/local_data/drainage.geojson new file mode 100644 index 00000000..b0566430 --- /dev/null +++ b/tests/data/local_data/drainage.geojson @@ -0,0 +1,9 @@ +{ +"type": "FeatureCollection", +"name": "drainage", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::32633" } }, +"features": [ +{ "type": "Feature", "properties": { "id": 1, "discharge": 10.0, "type": 1 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ 322021.879048462666105, 5045547.683568798005581 ], [ 322378.691038294811733, 5045133.781660593114793 ] ] ] } }, +{ "type": "Feature", "properties": { "id": 2, "discharge": 40.0, "type": 2 }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ 319160.246890009148046, 5045262.233976932242513 ], [ 319167.383129805792123, 5044976.784385066479445 ] ] ] } } +] +} diff --git a/tests/data/sfincs_test.yml b/tests/data/sfincs_test.yml index a32011dd..13f7c98f 100644 --- a/tests/data/sfincs_test.yml +++ b/tests/data/sfincs_test.yml @@ -48,6 +48,9 @@ setup_structures: structures: weir stype: thd +setup_drainage_structures: + structures: drainage + setup_river_inflow: hydrography: merit_hydro river_upa: 10 diff --git a/tests/data/sfincs_test/gis/drn.geojson b/tests/data/sfincs_test/gis/drn.geojson new file mode 100644 index 00000000..afebb1fc --- /dev/null +++ b/tests/data/sfincs_test/gis/drn.geojson @@ -0,0 +1,8 @@ +{ +"type": "FeatureCollection", +"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::32633" } }, +"features": [ +{ "type": "Feature", "properties": { "id": 1, "type": 1, "par1": 10.0, "par2": 0, "par3": 0, "par4": 0, "par5": 0 }, "geometry": { "type": "LineString", "coordinates": [ [ 322021.879048462666105, 5045547.683568798005581 ], [ 322378.691038294811733, 5045133.781660593114793 ] ] } }, +{ "type": "Feature", "properties": { "id": 2, "type": 2, "par1": 40.0, "par2": 0, "par3": 0, "par4": 0, "par5": 0 }, "geometry": { "type": "LineString", "coordinates": [ [ 319160.246890009148046, 5045262.233976932242513 ], [ 319167.383129805792123, 5044976.784385066479445 ] ] } } +] +} diff --git a/tests/data/sfincs_test/precip.nc b/tests/data/sfincs_test/precip_2d.nc similarity index 100% rename from tests/data/sfincs_test/precip.nc rename to tests/data/sfincs_test/precip_2d.nc diff --git a/tests/data/sfincs_test/sfincs.drn b/tests/data/sfincs_test/sfincs.drn new file mode 100644 index 00000000..d06a208d --- /dev/null +++ b/tests/data/sfincs_test/sfincs.drn @@ -0,0 +1,2 @@ +322021.8790 5045547.6836 322378.6910 5045133.7817 1 10.0000 0 0 0 0 +319160.2469 5045262.2340 319167.3831 5044976.7844 2 40.0000 0 0 0 0 diff --git a/tests/data/sfincs_test/sfincs.inp b/tests/data/sfincs_test/sfincs.inp index e00e1c20..c8428c88 100644 --- a/tests/data/sfincs_test/sfincs.inp +++ b/tests/data/sfincs_test/sfincs.inp @@ -52,4 +52,5 @@ outputformat = net cdnrb = 3 cdwnd = 0.0 28.0 50.0 cdval = 0.001 0.0025 0.0015 -netamprfile = precip.nc +netamprfile = precip_2d.nc +drnfile = sfincs.drn diff --git a/tests/data/sfincs_test/sfincs_his.nc b/tests/data/sfincs_test/sfincs_his.nc index 6aabea5b..34d4e889 100644 Binary files a/tests/data/sfincs_test/sfincs_his.nc and b/tests/data/sfincs_test/sfincs_his.nc differ diff --git a/tests/data/sfincs_test/sfincs_map.nc b/tests/data/sfincs_test/sfincs_map.nc index 928362e0..edbe093a 100644 Binary files a/tests/data/sfincs_test/sfincs_map.nc and b/tests/data/sfincs_test/sfincs_map.nc differ diff --git a/tests/test_1model_class.py b/tests/test_1model_class.py index 87af3ae2..920e60d2 100644 --- a/tests/test_1model_class.py +++ b/tests/test_1model_class.py @@ -106,7 +106,7 @@ def test_infiltration(tmpdir): assert np.isclose( mod1.grid["seff"].where(mod.mask > 0).sum(), 32.929287 * effective ) - assert np.isclose(mod1.grid["kr"].where(mod.mask > 0).sum(), 1.7879527) + assert np.isclose(mod1.grid["kr"].where(mod.mask > 0).sum(), 330.588) def test_structs(tmpdir): @@ -138,6 +138,28 @@ def test_structs(tmpdir): assert isfile(join(mod.root, "sfincs.weir")) +def test_drainage_structures(tmpdir): + root = TESTMODELDIR + mod = SfincsModel(root=root, mode="r+") + # read + mod.set_config("drnfile", "sfincs.drn") + mod.read_grid() + mod.read_geoms() + assert "drn" in mod.geoms + nr_drainage_structures = len(mod.geoms["drn"].index) + # write drn file only + tmp_root = str(tmpdir.join("drainage_struct_test")) + mod.set_root(tmp_root, mode="w") + mod.write_geoms(data_vars=["drn"]) + assert isfile(join(mod.root, "sfincs.drn")) + assert not isfile(join(mod.root, "sfincs.obs")) + fn_drn_gis = join(mod.root, "gis", "drn.geojson") + assert isfile(fn_drn_gis) + # add more drainage structures + mod.setup_drainage_structures(fn_drn_gis, merge=True) + assert len(mod.geoms["drn"].index) == nr_drainage_structures * 2 + + def test_results(): root = TESTMODELDIR mod = SfincsModel(root=root, mode="r")