From cef2b4bc0e43bbc532cdc7f25afc56d2f8b7bf00 Mon Sep 17 00:00:00 2001 From: marcovanbaar <42713153+marcovanbaar@users.noreply.github.com> Date: Thu, 31 Aug 2023 14:55:12 +0200 Subject: [PATCH 01/85] geotop_lithok_in_cross_section added alpha --- nlmod/plot/plot.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index cfd86939..1afccf18 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -205,7 +205,7 @@ def data_array(da, ds=None, ax=None, rotated=False, edgecolor=None, **kwargs): def geotop_lithok_in_cross_section( - line, gt=None, ax=None, legend=True, legend_loc=None, lithok_props=None, **kwargs + line, gt=None, ax=None, legend=True, legend_loc=None, lithok_props=None, alpha=None, **kwargs ): """PLot the lithoclass-data of GeoTOP in a cross-section. @@ -227,6 +227,9 @@ def geotop_lithok_in_cross_section( lithok_props : pd.DataFrame, optional A DataFrame containing the properties of the lithoclasses. Will call nlmod.read.geotop.get_lithok_props() when None. The default is None. + alpha : float, optional + Opacity for plot_array function, The default is None. + **kwargs : dict kwargs are passed onto DatasetCrossSection. @@ -264,7 +267,7 @@ def geotop_lithok_in_cross_section( colors.append(lithok_props.at[lithok, "color"]) cmap = ListedColormap(colors) norm = Normalize(-0.5, np.nanmax(array) + 0.5) - cs.plot_array(array, norm=norm, cmap=cmap) + cs.plot_array(array, norm=norm, cmap=cmap, alpha=alpha) if legend: # make a legend with dummy handles handles = [] From 97a85ae3f1d3894b5a74ca3f747936258bcdd1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Fri, 19 Jan 2024 09:43:31 +0100 Subject: [PATCH 02/85] Improve progressbar in docs (#313) * Change from tqdm import tqdm to from tqdm.auto import tqdm And update notebook 2 * Fix nbconvert version * Fix codacy issues And revert tqdm import * Add ipywidgets as dependency for the docs-build * Add docstring and tests to some ahn-methods --- docs/examples/02_surface_water.ipynb | 5 +- nlmod/read/ahn.py | 93 ++++++++++++++++++++++++++++ pyproject.toml | 3 +- tests/test_005_external_data.py | 18 ++++-- 4 files changed, 110 insertions(+), 9 deletions(-) diff --git a/docs/examples/02_surface_water.ipynb b/docs/examples/02_surface_water.ipynb index cf62249f..f5d8cb8ae 100644 --- a/docs/examples/02_surface_water.ipynb +++ b/docs/examples/02_surface_water.ipynb @@ -27,10 +27,7 @@ "import flopy\n", "import rioxarray\n", "import matplotlib.pyplot as plt\n", - "import nlmod\n", - "from geocube.api.core import make_geocube\n", - "from functools import partial\n", - "from geocube.rasterize import rasterize_image" + "import nlmod" ] }, { diff --git a/nlmod/read/ahn.py b/nlmod/read/ahn.py index c2444e79..cf88e869 100644 --- a/nlmod/read/ahn.py +++ b/nlmod/read/ahn.py @@ -1,6 +1,9 @@ import datetime as dt import logging +import matplotlib.pyplot as plt +import numpy as np +import geopandas as gpd import rasterio import rioxarray import xarray as xr @@ -86,6 +89,35 @@ def get_ahn_at_point( res=0.5, **kwargs, ): + """ + Get the height of the surface level at a certain point, defined by x and y. + + Parameters + ---------- + x : float + The x-coordinate fo the point. + y : float + The y-coordinate fo the point.. + buffer : float, optional + The buffer around x and y that is downloaded. The default is 0.75. + return_da : bool, optional + Return the downloaded DataArray when True. The default is False. + return_mean : bool, optional + Resturn the mean of all non-nan pixels within buffer. Return the center pixel + when False. The default is False. + identifier : str, optional + The identifier passed onto get_latest_ahn_from_wcs. The default is "dsm_05m". + res : float, optional + The resolution that is passed onto get_latest_ahn_from_wcs. The default is 0.5. + **kwargs : dict + kwargs are passed onto the method get_latest_ahn_from_wcs. + + Returns + ------- + float + The surface level value at the requested point. + + """ extent = [x - buffer, x + buffer, y - buffer, y + buffer] ahn = get_latest_ahn_from_wcs(extent, identifier=identifier, res=res, **kwargs) if return_da: @@ -99,6 +131,67 @@ def get_ahn_at_point( return ahn.data[int((ahn.shape[0] - 1) / 2), int((ahn.shape[1] - 1) / 2)] +def get_ahn_along_line(line, ahn=None, dx=None, num=None, method="linear", plot=False): + """ + Get the height of the surface level along a line. + + Parameters + ---------- + line : shapely.LineString + The line along which the surface level is calculated. + ahn : xr.DataArray, optional + The 2d DataArray containing surface level values. If None, ahn4-values are + downloaded from the web. The default is None. + dx : float, optional + The distance between the points along the line at which the surface level is + calculated. Only used when num is None. When dx is None, it is set to the + resolution of ahn. The default is None. + num : int, optional + If not None, the surface level is calculated at num equally spaced points along + the line. The default is None. + method : string, optional + The method to interpolate the 2d surface level values to the points along the + line. The default is "linear". + plot : bool, optional + if True, plot the 2d surface level, the line and the calculated heights. The + default is False. + + Returns + ------- + z : xr.DataArray + A DataArray with dimension s, containing surface level values along the line. + + """ + if ahn is None: + bbox = line.bounds + extent = [bbox[0], bbox[2], bbox[1], bbox[3]] + ahn = get_ahn4(extent) + if num is not None: + s = np.linspace(0.0, line.length, num) + else: + if dx is None: + dx = float(ahn.x[1] - ahn.x[0]) + s = np.arange(0.0, line.length, dx) + + x, y = zip(*[p.xy for p in line.interpolate(s)]) + + x = np.array(x)[:, 0] + y = np.array(y)[:, 0] + + x = xr.DataArray(x, dims="s", coords={"s": s}) + y = xr.DataArray(y, dims="s", coords={"s": s}) + z = ahn.interp(x=x, y=y, method=method) + + if plot: + _, ax = plt.subplots(figsize=(10, 10)) + ahn.plot(ax=ax) + gpd.GeoDataFrame(geometry=[line]).plot(ax=ax) + + _, ax = plt.subplots(figsize=(10, 10)) + z.plot(ax=ax) + return z + + @cache.cache_netcdf def get_latest_ahn_from_wcs( extent=None, diff --git a/pyproject.toml b/pyproject.toml index 330778cb..6729fa58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,9 +74,10 @@ rtd = [ "nlmod[full]", "ipython", "ipykernel", + "ipywidgets", "nbsphinx", "sphinx_rtd_theme==1.0.0", - "nbconvert>6.4.5", + "nbconvert==7.13.0", "netCDF4>=1.6.3", ] diff --git a/tests/test_005_external_data.py b/tests/test_005_external_data.py index f558b796..709bdaf0 100644 --- a/tests/test_005_external_data.py +++ b/tests/test_005_external_data.py @@ -1,4 +1,6 @@ import pandas as pd +import xarray as xr +from shapely.geometry import LineString import pytest import test_001_model @@ -65,9 +67,13 @@ def test_get_ahn3(): def test_get_ahn4(): extent = [98000.0, 100000.0, 494000.0, 496000.0] - da = nlmod.read.ahn.get_ahn4(extent) + ahn = nlmod.read.ahn.get_ahn4(extent) + assert isinstance(ahn, xr.DataArray) + assert not ahn.isnull().all(), "AHN only has nan values" - assert not da.isnull().all(), "AHN only has nan values" + line = LineString([(99000, 495000), (100000, 496000)]) + ahn_line = nlmod.read.ahn.get_ahn_along_line(line, ahn=ahn) + assert isinstance(ahn_line, xr.DataArray) def test_get_ahn(): @@ -80,6 +86,10 @@ def test_get_ahn(): assert not ahn_ds["ahn"].isnull().all(), "AHN only has nan values" +def test_get_ahn_at_point(): + nlmod.read.ahn.get_ahn_at_point(100010, 400010) + + def test_get_surface_water_ghb(): # model with sea ds = test_001_model.get_ds_from_cache("basic_sea_model") @@ -88,13 +98,13 @@ def test_get_surface_water_ghb(): sim = nlmod.sim.sim(ds) # create time discretisation - tdis = nlmod.sim.tdis(ds, sim) + nlmod.sim.tdis(ds, sim) # create groundwater flow model gwf = nlmod.gwf.gwf(ds, sim) # create ims - ims = nlmod.sim.ims(sim) + nlmod.sim.ims(sim) nlmod.gwf.dis(ds, gwf) From af10738684065a6c931330eddd6cab4e683ff0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 24 Jan 2024 10:35:25 +0100 Subject: [PATCH 03/85] Fix Issue 315 (#316) Landflag was calculated wrongly. A value of 0 was assigned to the first active cell even if it was active. This commit fixes this. --- nlmod/gwf/recharge.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nlmod/gwf/recharge.py b/nlmod/gwf/recharge.py index f8f29e55..fe06889c 100644 --- a/nlmod/gwf/recharge.py +++ b/nlmod/gwf/recharge.py @@ -387,10 +387,11 @@ def ds_to_uzf( if landflag is None: landflag = xr.full_like(ds["botm"], 0, dtype=int) # set the landflag in the top layer to 1 - fal = get_first_active_layer_from_idomain(idomain, nodata=0) + fal = get_first_active_layer_from_idomain(idomain) + # for the inactive domain set fal to 0 (setting nodata to 0 gives problems) + fal.data[fal == fal.nodata] = 0 landflag[fal] = 1 - - # set landflag to 0 in inactivate domain + # set landflag to 0 in inactivate domain (where we set fal to 0 before) landflag = xr.where(idomain > 0, landflag, 0) # determine ivertcon, by setting its value to iuzno of the layer below From d730e265cf5a929f8555f71134d2f0ab6653cfd2 Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Wed, 24 Jan 2024 17:17:56 +0100 Subject: [PATCH 04/85] Update webservices.py (#314) Two issues are solved: - If values for a large area are prompted, the query url gets very long and is not accepted by HHNK polderpeil server. Is now queried in chunks - An error in the database of HHNK resulted in duplicate peil definitions. Only use the unambigious entries and leave a note in the logs. --- nlmod/read/webservices.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/nlmod/read/webservices.py b/nlmod/read/webservices.py index 64c72742..cd47e59d 100644 --- a/nlmod/read/webservices.py +++ b/nlmod/read/webservices.py @@ -150,22 +150,53 @@ def arcrest( else: gdf = gpd.GeoDataFrame.from_features(features, crs=sr) if table is not None: - url_query = f"{url}/{table.pop('id')}/query" - pgbids = ",".join([str(v) for v in gdf["OBJECTID"].values]) - params["where"] = f"PEILGEBIEDVIGERENDID IN ({pgbids})" params["f"] = "json" - data = _get_data(url_query, params, timeout=timeout) + url_query = f"{url}/{table.pop('id')}/query" + + # loop over chunks of 100 pgbids. Long where clauses can cause + # the request to fail. 1300 pgbids fails but 130 works + chunk_size = 100 + ids_chunks = [ + gdf["OBJECTID"].values[i : i + chunk_size] + for i in range(0, len(gdf), chunk_size) + ] + data = {} + features = [] + + for ids_chunk in ids_chunks: + pgbids = ",".join([str(v) for v in ids_chunk]) + where = f"PEILGEBIEDVIGERENDID IN ({pgbids})" + params["where"] = where + _data = _get_data(url_query, params, timeout=timeout, **kwargs) + + data.update(_data) + features.extend(_data["features"]) + + assert "exceededTransferLimit" not in data, "exceededTransferLimit" + data["features"] = features + df = pd.DataFrame( [feature["attributes"] for feature in data["features"]] ) + # add peilen to gdf for col, convert_dic in table.items(): df[col].replace(convert_dic, inplace=True) df.set_index(col, inplace=True) + for oid in gdf["OBJECTID"]: insert_s = df.loc[ df["PEILGEBIEDVIGERENDID"] == oid, "WATERHOOGTE" ] + if insert_s.index.duplicated().any(): + # Error in the database. Reported to Doeke HHNK 20230123 + # Continue with unambiguous values + dup = set(insert_s.index[insert_s.index.duplicated()]) + logger.warning( + f"Duplicate {dup} values for PEILGEBIEDVIGERENDID {oid} while prompting {url}" + ) + insert_s = insert_s[~insert_s.index.duplicated(keep=False)] + gdf.loc[ gdf["OBJECTID"] == oid, insert_s.index ] = insert_s.values From 904f269e71b109e152580ac967369dd11098a0ee Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Tue, 30 Jan 2024 13:23:08 +0100 Subject: [PATCH 05/85] HHNK revert to old url (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * HHNK revert to old url * Fix failing test (unrelated to other change in this PR) --------- Co-authored-by: Ruben Caljé --- nlmod/gwf/surface_water.py | 12 +++++++++++- nlmod/read/waterboard.py | 14 +------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index e5f6eca1..6e7bf0b3 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -571,7 +571,7 @@ def get_gdf_stage(gdf, season="winter"): def download_level_areas( - gdf, extent=None, config=None, raise_exceptions=True, **kwargs + gdf, extent=None, config=None, raise_exceptions=True, drop_duplicates=True, **kwargs ): """Download level areas (peilgebieden) of bronhouders. @@ -590,6 +590,9 @@ def download_level_areas( Raises exceptions, mostly caused by a webservice that is offline. When raise_exceptions is False, the error is raised as a warning. The default is True. + drop_duplicates : bool, optional + Drop features with a duplicate index, keeping the first occurence. The default + is True. Returns ------- @@ -610,6 +613,13 @@ def download_level_areas( if len(lawb) == 0: logger.info(f"No {data_kind} for {wb} found within model area") continue + if drop_duplicates: + mask = lawb.index.duplicated() + if mask.any(): + msg = "Dropping {} level area(s) of {} with duplicate indexes" + logger.warning(msg.format(mask.sum(), wb)) + lawb = lawb.loc[~mask] + la[wb] = lawb mask = ~la[wb].is_valid if mask.any(): diff --git a/nlmod/read/waterboard.py b/nlmod/read/waterboard.py index b44d44c2..8abe21a0 100644 --- a/nlmod/read/waterboard.py +++ b/nlmod/read/waterboard.py @@ -200,19 +200,7 @@ def get_configuration(): "bottom_height": "WS_BODEMHOOGTE", }, "level_areas": { - "url": "https://kaarten.hhnk.nl/arcgis/rest/services/ws/ws_peilgebieden_vigerend/MapServer", - "layer": 4, - "table": { - "id": 6, - "SOORTSTREEFPEIL": { - 901: "STREEFPEIL_JAARROND", # vast peilbeheer - 902: "STREEFPEIL_WINTER", - 903: "STREEFPEIL_ZOMER", - 904: "STREEFPEIL_JAARROND", # dynamisch peilbeheer - 905: "ONDERGRENS_JAARROND", - 906: "BOVENGRENS_JAARROND", - }, - }, + "url": "https://kaarten.hhnk.nl/arcgis/rest/services/NHFLO/Peilgebied_beheerregister/MapServer", "summer_stage": [ "ZOMER", "STREEFPEIL_ZOMER", From 30197ddd0ba660410041f3aa27ef859a44f0c926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 31 Jan 2024 11:07:53 +0100 Subject: [PATCH 06/85] Add support for NHI GWO database (#319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for NHI GWO database with extraction data * Add timeout to solve codacy-issues * docs typos * Make nhi gwo methods more robust Add checks for empty empty content Determine empty lines in get_gwo_wells, to be used in skiprows * Fix codacy issue --------- Co-authored-by: Davíd Brakenhoff --- .github/workflows/ci.yml | 6 ++ nlmod/read/nhi.py | 215 ++++++++++++++++++++++++++++++++++++++- tests/test_021_nhi.py | 43 ++++++++ 3 files changed, 263 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49e3f0a2..c4a524e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,11 +46,17 @@ jobs: - name: Run notebooks if: ${{ github.event_name == 'push' }} + env: + NHI_GWO_USERNAME: ${{ secrets.NHI_GWO_USERNAME}} + NHI_GWO_PASSWORD: ${{ secrets.NHI_GWO_PASSWORD}} run: | py.test ./tests -m "not notebooks" - name: Run tests only if: ${{ github.event_name == 'pull_request' }} + env: + NHI_GWO_USERNAME: ${{ secrets.NHI_GWO_USERNAME}} + NHI_GWO_PASSWORD: ${{ secrets.NHI_GWO_PASSWORD}} run: | py.test ./tests -m "not notebooks" diff --git a/nlmod/read/nhi.py b/nlmod/read/nhi.py index 858e3a16..d58c1819 100644 --- a/nlmod/read/nhi.py +++ b/nlmod/read/nhi.py @@ -1,8 +1,12 @@ import logging import os +import io +import requests import numpy as np -import requests +import pandas as pd +import geopandas as gpd + import rioxarray from ..dims.resample import structured_da_to_ds @@ -173,3 +177,212 @@ def add_buisdrainage( ds[depth_var] = ds[depth_var] / 100.0 return ds + + +def get_gwo_wells( + username, + password, + n_well_filters=1_000, + well_site=None, + organisation=None, + status=None, + well_index="Name", + timeout=120, + **kwargs, +): + """ + Get metadata of extraction wells from the NHI GWO database + + Parameters + ---------- + username : str + The username of the NHI GWO database. To retrieve a username and password visit + https://gwo.nhi.nu/register/. + password : str + The password of the NHI GWO database. To retrieve a username and password visit + https://gwo.nhi.nu/register/. + n_well_filters : int, optional + The number of wells that are requested per page. This number determines in how + many pieces the request is split. The default is 1000. + organisation : str, optional + The organisation that manages the wells. If not None, the organisation will be + used to filter the wells. The default is None. + well_site : str, optional + The name of well site the wells belong to. If not None, the well site will be + used to filter the wells. The default is None. + status : str, optional + The status of the wells. If not None, the status will be used to filter the + wells. Possible values are "Active", "Inactive" or "Abandoned". The default is + None. + well_index : str, tuple or list, optional + The column(s) in the resulting GeoDataFrame that is/are used as the index of + this GeoDataFrame. The default is "Name". + timeout : int, optional + The timeout time (in seconds) for requests to the database. The default is + 120 seconds. + **kwargs : dict + Kwargs are passed as additional parameters in the request to the database. For + available parameters see https://gwo.nhi.nu/api/v1/download/. + + Returns + ------- + gdf : geopandas.GeoDataFrame + A GeoDataFrame containing the properties of the wells and their filters. + + """ + # zie https://gwo.nhi.nu/api/v1/download/ + url = "https://gwo.nhi.nu/api/v1/well_filters/" + + page = 1 + properties = [] + while page is not None: + params = {"format": "csv", "n_well_filters": n_well_filters, "page": page} + if status is not None: + params["well__status"] = status + if organisation is not None: + params["well__organization"] = organisation + if well_site is not None: + params["well__site"] = well_site + params.update(kwargs) + + r = requests.get(url, auth=(username, password), params=params, timeout=timeout) + content = r.content.decode("utf-8") + if len(content) == 0: + if page == 1: + msg = "No extraction wells found for the requested parameters" + raise ValueError(msg) + else: + # the number of wells is exactly a multiple of n_well_filters + page = None + continue + lines = content.split("\n") + empty_lines = np.where([set(line) == set(";") for line in lines])[0] + assert len(empty_lines) == 1, "Returned extraction wells cannot be interpreted" + skiprows = list(range(empty_lines[0] + 1)) + [empty_lines[0] + 2] + df = pd.read_csv(io.StringIO(content), skiprows=skiprows, sep=";") + properties.append(df) + + if len(df) == n_well_filters: + page += 1 + else: + page = None + df = pd.concat(properties) + geometry = gpd.points_from_xy(df.XCoordinate, df.YCoordinate) + gdf = gpd.GeoDataFrame(df, geometry=geometry) + if well_index is not None: + gdf = gdf.set_index(well_index) + return gdf + + +def get_gwo_measurements( + username, + password, + n_measurements=10_000, + well_site=None, + well_index="Name", + measurement_index=("Name", "DateTime"), + timeout=120, + **kwargs, +): + """ + Get extraction rates and metadata of wells from the NHI GWO database + + Parameters + ---------- + username : str + The username of the NHI GWO database. To retrieve a username and password visit + https://gwo.nhi.nu/register/. + password : str + The password of the NHI GWO database. To retrieve a username and password visit + https://gwo.nhi.nu/register/. + n_measurements : int, optional + The number of measurements that are requested per page, with a maximum of + 200,000. This number determines in how many pieces the request is split. The + default is 10,000. + well_site : str, optional + The name of well site the wells belong to. If not None, the well site will be + used to filter the wells. The default is None. + well_index : str, tuple or list, optional + The column(s) in the resulting GeoDataFrame that is/are used as the index of + this GeoDataFrame. The default is "Name". + measurement_index : str, tuple or list, optional, optional + The column(s) in the resulting measurement-DataFrame that is/are used as the + index of this DataFrame. The default is ("Name", "DateTime"). + timeout : int, optional + The timeout time (in seconds) of requests to the database. The default is + 120 seconds. + **kwargs : dict + Kwargs are passed as additional parameters in the request to the database. For + available parameters see https://gwo.nhi.nu/api/v1/download/. + + Returns + ------- + measurements : pandas.DataFrame + A DataFrame containing the extraction rates of the wells in the database. + gdf : geopandas.GeoDataFrame + A GeoDataFrame containing the properties of the wells and their filters. + + """ + url = "http://gwo.nhi.nu/api/v1/measurements/" + properties = [] + measurements = [] + page = 1 + while page is not None: + params = { + "format": "csv", + "n_measurements": n_measurements, + "page": page, + } + if well_site is not None: + params["filter__well__site"] = well_site + params.update(kwargs) + r = requests.get(url, auth=(username, password), params=params, timeout=timeout) + + content = r.content.decode("utf-8") + if len(content) == 0: + if page == 1: + msg = "No extraction rates found for the requested parameters" + raise (ValueError(msg)) + else: + # the number of measurements is exactly a multiple of n_measurements + page = None + continue + lines = content.split("\n") + empty_lines = np.where([set(line) == set(";") for line in lines])[0] + assert len(empty_lines) == 2, "Returned extraction rates cannot be interpreted" + + # read properties + skiprows = list(range(empty_lines[0] + 1)) + [empty_lines[0] + 2] + nrows = empty_lines[1] - empty_lines[0] - 3 + df = pd.read_csv(io.StringIO(content), sep=";", skiprows=skiprows, nrows=nrows) + properties.append(df) + + # read measurements + skiprows = list(range(empty_lines[1] + 1)) + [empty_lines[1] + 2] + df = pd.read_csv( + io.StringIO(content), + skiprows=skiprows, + sep=";", + parse_dates=["DateTime"], + dayfirst=True, + ) + measurements.append(df) + if len(df) == n_measurements: + page += 1 + else: + page = None + measurements = pd.concat(measurements) + # drop columns without measurements + measurements = measurements.loc[:, ~measurements.isna().all()] + if measurement_index is not None: + if isinstance(measurement_index, tuple): + measurement_index = list(measurement_index) + measurements = measurements.set_index(["Name", "DateTime"]) + df = pd.concat(properties) + geometry = gpd.points_from_xy(df.XCoordinate, df.YCoordinate) + gdf = gpd.GeoDataFrame(df, geometry=geometry) + if well_index is not None: + gdf = gdf.set_index(well_index) + # drop duplicate properties from multiple pages + gdf = gdf[~gdf.index.duplicated()] + return measurements, gdf diff --git a/tests/test_021_nhi.py b/tests/test_021_nhi.py index af768339..6f5e06f8 100644 --- a/tests/test_021_nhi.py +++ b/tests/test_021_nhi.py @@ -1,8 +1,10 @@ import os import numpy as np +import geopandas as gpd import tempfile import nlmod import pytest +import matplotlib.pyplot as plt tmpdir = tempfile.gettempdir() @@ -20,3 +22,44 @@ def test_buidrainage(): # assert that all locations with a positive conductance also have a specified depth mask = ds["buisdrain_cond"] > 0 assert np.all(~np.isnan(ds["buisdrain_depth"].data[mask])) + + +def test_gwo(): + username = os.environ["NHI_GWO_USERNAME"] + password = os.environ["NHI_GWO_PASSWORD"] + + # download all wells from Brabant Water + wells = nlmod.read.nhi.get_gwo_wells( + username=username, password=password, organisation="Brabant Water" + ) + assert isinstance(wells, gpd.GeoDataFrame) + + # download extractions from well "13-PP016" of pomping station Veghel + measurements, gdf = nlmod.read.nhi.get_gwo_measurements( + username, password, well_site="veghel", filter__well__name="13-PP016" + ) + assert measurements.reset_index()["Name"].isin(gdf.index).all() + + +@pytest.mark.skip("too slow") +def test_gwo_entire_pumping_station(): + username = os.environ["NHI_GWO_USERNAME"] + password = os.environ["NHI_GWO_PASSWORD"] + measurements, gdf = nlmod.read.nhi.get_gwo_measurements( + username, + password, + well_site="veghel", + ) + assert measurements.reset_index()["Name"].isin(gdf.index).all() + + ncols = 3 + nrows = int(np.ceil(len(gdf.index) / ncols)) + f, axes = plt.subplots( + nrows=nrows, ncols=ncols, figsize=(10, 10), sharex=True, sharey=True + ) + axes = axes.ravel() + for name, ax in zip(gdf.index, axes): + measurements.loc[name, "Volume"].plot(ax=ax) + ax.set_xlabel("") + ax.set_title(name) + f.tight_layout(pad=0.0) From 9cc59d369dc4f41db4828d1ddebd698119fa6c37 Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Fri, 2 Feb 2024 14:59:06 +0100 Subject: [PATCH 07/85] ds_to_gridprops() to also pass unaltered datavars (#318) * ds_to_gridprops() to also pass unaltered datavars ds_to_gridprops() is called by refine(). If the time settings were configured prior to calling the refine function, the refine function silently threw away the steady array, as it is non-numeric. Not anymore. * Minor edit to ds_to_gridprops --- nlmod/dims/grid.py | 50 +++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 1e2e38fe..2cdbe8f0 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -509,10 +509,12 @@ def refine( return ds -def ds_to_gridprops(ds_in, gridprops, method="nearest", nodata=-1): +def ds_to_gridprops(ds_in, gridprops, method="nearest", icvert_nodata=-1): """resample a dataset (xarray) on an structured grid to a new dataset with a vertex grid. + Returns a dataset with resampled variables and the untouched variables. + Parameters ---------- ds_in : xarray.Dataset @@ -523,15 +525,14 @@ def ds_to_gridprops(ds_in, gridprops, method="nearest", nodata=-1): definition of the vertex grid. method : str, optional type of interpolation used to resample. The default is 'nearest'. - nodata : int, optional + icvert_nodata : int, optional integer to represent nodata-values in cell2d array. Defaults to -1. Returns ------- ds_out : xarray.Dataset - dataset with dimensions (layer, icell2d). + dataset with resampled variables and the untouched variables. """ - logger.info("resample model Dataset to vertex modelgrid") assert isinstance(ds_in, xr.core.dataset.Dataset) @@ -540,41 +541,50 @@ def ds_to_gridprops(ds_in, gridprops, method="nearest", nodata=-1): x = xr.DataArray(xyi[:, 0], dims=("icell2d",)) y = xr.DataArray(xyi[:, 1], dims=("icell2d",)) - # drop non-numeric data variables - for key, dtype in ds_in.dtypes.items(): - if not np.issubdtype(dtype, np.number): - ds_in = ds_in.drop_vars(key) - logger.info( - f"cannot convert data variable {key} to refined dataset because of non-numeric dtype" - ) - if method in ["nearest", "linear"]: - # resample the entire dataset in one line + # resample the entire dataset in one line. Leaves not_interp_vars untouched ds_out = ds_in.interp(x=x, y=y, method=method, kwargs={"fill_value": None}) + else: - ds_out = xr.Dataset(coords={"layer": ds_in.layer.data, "x": x, "y": y}) + # apply method to numeric data variables + interp_vars = [] + not_interp_vars = [] + for key, var in ds_in.items(): + if "x" in var.dims or "y" in var.dims: + if np.issubdtype(var.dtype, np.number): + interp_vars.append(key) + else: + logger.info( + f"Data variable {key} has spatial coordinates but it cannot be refined " + "because of its non-numeric dtype. It is not available in the output Dataset." + ) + else: + not_interp_vars.append(key) + + ds_out = ds_in[not_interp_vars] + ds_out.coords.update({"layer": ds_in.layer, "x": x, "y": y}) # add other variables - for data_var in ds_in.data_vars: - data_arr = structured_da_to_ds(ds_in[data_var], ds_out, method=method) - ds_out[data_var] = data_arr + for not_interp_var in not_interp_vars: + ds_out[not_interp_var] = structured_da_to_ds( + da=ds_in[not_interp_var], ds=ds_out, method=method, nodata=np.NaN + ) if "area" in gridprops: if "area" in ds_out: ds_out = ds_out.drop_vars("area") + # only keep the first layer of area area = gridprops["area"][: len(ds_out["icell2d"])] ds_out["area"] = ("icell2d", area) # add information about the vertices - ds_out = gridprops_to_vertex_ds(gridprops, ds_out, nodata=nodata) + ds_out = gridprops_to_vertex_ds(gridprops, ds_out, nodata=icvert_nodata) # then finally change the gridtype in the attributes ds_out.attrs["gridtype"] = "vertex" - return ds_out - def get_xyi_icell2d(gridprops=None, ds=None): """Get x and y coordinates of the cell mids from the cellids in the grid properties. From 0feb8e0b70279809605ca5d3c6358bfb422fe917 Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Mon, 12 Feb 2024 14:20:25 +0100 Subject: [PATCH 08/85] Zero layer thickness layers are considered active due to float rounding (#324) * Zero layer thickness layers are considered active due to float rounding Subtracting two botms to calculate the thickness results is some layers with a thickness of ~1e-9. This rounding later results in a problem because this positive thickness leads to an idomain of 1. This can either be solved in `nlmod.layers.calculating_thickness()` or in `nlmod.layers.get_idomain()`. What do you prefer? * Remove trailing whitespace * Applied patch also to get_idomain --- nlmod/dims/layers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nlmod/dims/layers.py b/nlmod/dims/layers.py index f01b8549..a3c44d7b 100644 --- a/nlmod/dims/layers.py +++ b/nlmod/dims/layers.py @@ -47,6 +47,10 @@ def calculate_thickness(ds, top="top", bot="botm"): thickness[lay] = ds[bot][lay - 1] - ds[bot][lay] else: raise ValueError("2d top should have same last dimension as bot") + + # subtracting floats can result in rounding errors. Mainly anoying for zero thickness layers. + thickness = thickness.where(~np.isclose(thickness, 0.), 0.) + if isinstance(ds[bot], xr.DataArray): thickness.name = "thickness" if hasattr(ds[bot], "long_name"): @@ -1310,6 +1314,8 @@ def get_idomain(ds): idomain.attrs.clear() # set idomain of cells with a positive thickness to 1 thickness = calculate_thickness(ds) + # subtracting floats can result in rounding errors. Mainly anoying for zero thickness layers. + thickness = thickness.where(~np.isclose(thickness, 0.), 0.) idomain.data[thickness.data > 0.0] = 1 # set idomain above/below the first/last active layer to 0 idomain.data[idomain.where(idomain > 0).ffill(dim="layer").isnull()] = 0 From 6d5bddc1de2c8a41d867d75ff00e480ea9ca15c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Thu, 15 Feb 2024 11:42:45 +0100 Subject: [PATCH 09/85] Fix issue 322 (#323) --- nlmod/plot/plot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index 176dc81b..0b5f4fc5 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -40,7 +40,11 @@ def modelgrid(ds, ax=None, **kwargs): _, ax = plt.subplots(figsize=(10, 10)) ax.axis("scaled") modelgrid = modelgrid_from_ds(ds) + extent = None if ax.get_autoscale_on() else ax.axis() modelgrid.plot(ax=ax, **kwargs) + if extent is not None: + ax.axis(extent) + return ax From 3a2bcd750e908cc20dc36b0c3b6a7eb07c39ed2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 28 Feb 2024 16:01:59 +0100 Subject: [PATCH 10/85] Improve grid rotation (#325) * Some changes to better implement grid rotation for structured grids * Add a test for a rotated grid * Add extra tests for grids And fix wrong values for xc and yc for rotated vertex grids Also fix gdf_to_count_da for rotated grids * Test vertex_da_to_ds and fillnan_da * Update tests to fix failing tests * Improve docstrings of xorigin, yorigin and angrot --- nlmod/dims/base.py | 87 +++++++++++------ nlmod/dims/grid.py | 8 +- nlmod/dims/resample.py | 54 +++++++---- nlmod/plot/plot.py | 2 +- tests/test_026_grid.py | 215 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 318 insertions(+), 48 deletions(-) create mode 100644 tests/test_026_grid.py diff --git a/nlmod/dims/base.py b/nlmod/dims/base.py index 76a6e006..c652d246 100644 --- a/nlmod/dims/base.py +++ b/nlmod/dims/base.py @@ -113,13 +113,20 @@ def to_model_ds( fill_value_kv : int or float, optional use this value for kv if there is no data. The default is 0.1. xorigin : int or float, optional - lower left x coordinate of the model grid only used if angrot != 0. - Default is 0.0. + lower left x coordinate of the model grid. When angrot == 0, xorigin is added to + the first two values of extent. Otherwise it is the x-coordinate of the point + the grid is rotated around, and xorigin is added to the Dataset-attributes. + The default is 0.0. yorigin : int or float, optional - lower left y coordinate of the model grid only used if angrot != 0. - Default is 0.0. + lower left y coordinate of the model grid. When angrot == 0, yorigin is added to + the last two values of extent. Otherwise it is the y-coordinate of the point + the grid is rotated around, and yorigin is added to the Dataset-attributes. + The default is 0.0. angrot : int or float, optinal - the rotation of the grid in counter clockwise degrees, default is 0.0 + the rotation of the grid in counter clockwise degrees. When angrot != 0 the grid + is rotated, and all coordinates of the model are in model coordinates. See + https://nlmod.readthedocs.io/en/stable/examples/11_grid_rotation.html for more + infomation. The default is 0.0. drop_attributes : bool, optional if True drop the attributes from the layer model dataset. Otherwise keep the attributes. Default is True. @@ -274,13 +281,21 @@ def _get_structured_grid_ds( A 2D array of the top elevation of the grid cells. Default is NaN. botm : array_like, optional A 3D array of the bottom elevation of the grid cells. Default is NaN. - xorigin : float, optional - The x-coordinate origin of the grid. Default is 0.0. - yorigin : float, optional - The y-coordinate origin of the grid. Default is 0.0. - angrot : float, optional - The counter-clockwise rotation angle of the grid, in degrees. - Default is 0. + xorigin : int or float, optional + lower left x coordinate of the model grid. When angrot == 0, xorigin is added to + the first two values of extent. Otherwise it is the x-coordinate of the point + the grid is rotated around, and xorigin is added to the Dataset-attributes. + The default is 0.0. + yorigin : int or float, optional + lower left y coordinate of the model grid. When angrot == 0, yorigin is added to + the last two values of extent. Otherwise it is the y-coordinate of the point + the grid is rotated around, and yorigin is added to the Dataset-attributes. + The default is 0.0. + angrot : int or float, optinal + the rotation of the grid in counter clockwise degrees. When angrot != 0 the grid + is rotated, and all coordinates of the model are in model coordinates. See + https://nlmod.readthedocs.io/en/stable/examples/11_grid_rotation.html for more + infomation. The default is 0.0. attrs : dict, optional A dictionary of attributes to add to the xarray dataset. Default is an empty dictionary. @@ -406,13 +421,21 @@ def _get_vertex_grid_ds( A 2D array of the top elevation of the grid cells. Default is NaN. botm : array_like, optional A 3D array of the bottom elevation of the grid cells. Default is NaN. - xorigin : float, optional - The x-coordinate origin of the grid. Default is 0.0. - yorigin : float, optional - The y-coordinate origin of the grid. Default is 0.0. - angrot : float, optional - The counter-clockwise rotation angle of the grid, in degrees. - Default is 0.0. + xorigin : int or float, optional + lower left x coordinate of the model grid. When angrot == 0, xorigin is added to + the first two values of extent. Otherwise it is the x-coordinate of the point + the grid is rotated around, and xorigin is added to the Dataset-attributes. + The default is 0.0. + yorigin : int or float, optional + lower left y coordinate of the model grid. When angrot == 0, yorigin is added to + the last two values of extent. Otherwise it is the y-coordinate of the point + the grid is rotated around, and yorigin is added to the Dataset-attributes. + The default is 0.0. + angrot : int or float, optinal + the rotation of the grid in counter clockwise degrees. When angrot != 0 the grid + is rotated, and all coordinates of the model are in model coordinates. See + https://nlmod.readthedocs.io/en/stable/examples/11_grid_rotation.html for more + infomation. The default is 0.0. attrs : dict, optional A dictionary of attributes to add to the xarray dataset. Default is an empty dictionary. @@ -546,15 +569,21 @@ def get_ds( is a float or a list/array of len(layer). The default is 1.0. crs : int, optional The coordinate reference system of the model. The default is 28992. - xorigin : float, optional - x-position of the lower-left corner of the model grid. Only used when angrot is - not 0. The defauls is 0.0. - yorigin : float, optional - y-position of the lower-left corner of the model grid. Only used when angrot is - not 0. The defauls is 0.0. - angrot : float, optional - counter-clockwise rotation angle (in degrees) of the lower-left corner of the - model grid. The default is 0.0 + xorigin : int or float, optional + lower left x coordinate of the model grid. When angrot == 0, xorigin is added to + the first two values of extent. Otherwise it is the x-coordinate of the point + the grid is rotated around, and xorigin is added to the Dataset-attributes. + The default is 0.0. + yorigin : int or float, optional + lower left y coordinate of the model grid. When angrot == 0, yorigin is added to + the last two values of extent. Otherwise it is the y-coordinate of the point + the grid is rotated around, and yorigin is added to the Dataset-attributes. + The default is 0.0. + angrot : int or float, optinal + the rotation of the grid in counter clockwise degrees. When angrot != 0 the grid + is rotated, and all coordinates of the model are in model coordinates. See + https://nlmod.readthedocs.io/en/stable/examples/11_grid_rotation.html for more + infomation. The default is 0.0. attrs : dict, optional Attributes of the model dataset. The default is None. extrapolate : bool, optional @@ -660,7 +689,7 @@ def check_variable(var, shape): ds, model_name=model_name, model_ws=model_ws, - extent=extent, + extent=attrs["extent"], delr=delr, delc=delc, drop_attributes=False, diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 2cdbe8f0..8893ac05 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -5,6 +5,7 @@ can be used as input for a MODFLOW package - fill, interpolate and resample grid data """ + import logging import os import warnings @@ -569,6 +570,10 @@ def ds_to_gridprops(ds_in, gridprops, method="nearest", icvert_nodata=-1): ds_out[not_interp_var] = structured_da_to_ds( da=ds_in[not_interp_var], ds=ds_out, method=method, nodata=np.NaN ) + has_rotation = "angrot" in ds_out.attrs and ds_out.attrs["angrot"] != 0.0 + if has_rotation: + affine = get_affine_mod_to_world(ds_out) + ds_out["xc"], ds_out["yc"] = affine * (ds_out.x, ds_out.y) if "area" in gridprops: if "area" in ds_out: @@ -585,6 +590,7 @@ def ds_to_gridprops(ds_in, gridprops, method="nearest", icvert_nodata=-1): ds_out.attrs["gridtype"] = "vertex" return ds_out + def get_xyi_icell2d(gridprops=None, ds=None): """Get x and y coordinates of the cell mids from the cellids in the grid properties. @@ -1536,7 +1542,7 @@ def gdf_to_count_da(gdf, ds, ix=None, buffer=0.0, **kwargs): # build list of gridcells if ix is None: - modelgrid = modelgrid_from_ds(ds) + modelgrid = modelgrid_from_ds(ds, rotated=False) ix = GridIntersect(modelgrid, method="vertex") if ds.gridtype == "structured": diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index df309016..c5b536af 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -117,13 +117,20 @@ def ds_to_structured_grid( delc : int or float cell size along columns of the desired grid (dy). xorigin : int or float, optional - lower left x coordinate of the model grid only used if angrot != 0. - Default is 0.0. + lower left x coordinate of the model grid. When angrot == 0, xorigin is added to + the first two values of extent. Otherwise it is the x-coordinate of the point + the grid is rotated around, and xorigin is added to the Dataset-attributes. + The default is 0.0. yorigin : int or float, optional - lower left y coordinate of the model grid only used if angrot != 0. - Default is 0.0. + lower left y coordinate of the model grid. When angrot == 0, yorigin is added to + the last two values of extent. Otherwise it is the y-coordinate of the point + the grid is rotated around, and yorigin is added to the Dataset-attributes. + The default is 0.0. angrot : int or float, optinal - the rotation of the grid in counter clockwise degrees, default is 0.0 + the rotation of the grid in counter clockwise degrees. When angrot != 0 the grid + is rotated, and all coordinates of the model are in model coordinates. See + https://nlmod.readthedocs.io/en/stable/examples/11_grid_rotation.html for more + infomation. The default is 0.0. method : str, optional type of interpolation used to resample. Sea structured_da_to_ds for possible values of method. The default is 'nearest'. @@ -139,11 +146,11 @@ def ds_to_structured_grid( if delc is None: delc = delr - x, y = get_xy_mid_structured(extent, delr, delc) - attrs = ds_in.attrs.copy() _set_angrot_attributes(extent, xorigin, yorigin, angrot, attrs) + x, y = get_xy_mid_structured(attrs["extent"], delr, delc) + # add new attributes attrs["gridtype"] = "structured" if isinstance(delr, numbers.Number) and isinstance(delc, numbers.Number): @@ -171,15 +178,21 @@ def _set_angrot_attributes(extent, xorigin, yorigin, angrot, attrs): ---------- extent : list, tuple or np.array of length 4 extent (xmin, xmax, ymin, ymax) of the desired grid. - xorigin : float - x-position of the lower-left corner of the model grid. Only used when angrot is - not 0. - yorigin : float - y-position of the lower-left corner of the model grid. Only used when angrot is - not 0. - angrot : float - counter-clockwise rotation angle (in degrees) of the lower-left corner of the - model grid. + xorigin : int or float, optional + lower left x coordinate of the model grid. When angrot == 0, xorigin is added to + the first two values of extent. Otherwise it is the x-coordinate of the point + the grid is rotated around, and xorigin is added to the Dataset-attributes. + The default is 0.0. + yorigin : int or float, optional + lower left y coordinate of the model grid. When angrot == 0, yorigin is added to + the last two values of extent. Otherwise it is the y-coordinate of the point + the grid is rotated around, and yorigin is added to the Dataset-attributes. + The default is 0.0. + angrot : int or float, optinal + the rotation of the grid in counter clockwise degrees. When angrot != 0 the grid + is rotated, and all coordinates of the model are in model coordinates. See + https://nlmod.readthedocs.io/en/stable/examples/11_grid_rotation.html for more + infomation. The default is 0.0. attrs : dict Attributes of a model dataset. @@ -308,6 +321,11 @@ def fillnan_da_vertex_grid(xar_in, ds=None, x=None, y=None, method="nearest"): can be slow if the xar_in is a large raster """ + if xar_in.dims != ("icell2d",): + raise ValueError( + f"expected dataarray with dimensions ('icell2d'), got dimensions -> {xar_in.dims}" + ) + # get list of coordinates from all points in raster if x is None: x = ds["x"].data @@ -431,7 +449,9 @@ def dim_to_regular_dim(da, dims, z): coords = dict(da.coords) coords["x"] = ds.x coords["y"] = ds.y - coords.pop("icell2d") + for key in list(coords): + if "icell2d" in coords[key].dims: + coords.pop(key) else: # just use griddata z = griddata(points, da.data, xi, method=method) diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index 0b5f4fc5..9fc3dddb 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -38,7 +38,7 @@ def surface_water(model_ds, ax=None, **kwargs): def modelgrid(ds, ax=None, **kwargs): if ax is None: _, ax = plt.subplots(figsize=(10, 10)) - ax.axis("scaled") + ax.set_aspect("auto") modelgrid = modelgrid_from_ds(ds) extent = None if ax.get_autoscale_on() else ax.axis() modelgrid.plot(ax=ax, **kwargs) diff --git a/tests/test_026_grid.py b/tests/test_026_grid.py new file mode 100644 index 00000000..b646e5a0 --- /dev/null +++ b/tests/test_026_grid.py @@ -0,0 +1,215 @@ +import tempfile +import os +import numpy as np +import xarray as xr +import geopandas as gpd +import matplotlib.pyplot as plt +import nlmod + +model_ws = os.path.join(tempfile.gettempdir(), "test_grid") +extent = [98000.0, 99000.0, 489000.0, 490000.0] + + +def get_bgt(): + fname = os.path.join(model_ws, "bgt.gpkg") + if not os.path.isfile(fname): + if not os.path.isdir(model_ws): + os.makedirs(model_ws) + bgt = nlmod.read.bgt.get_bgt(extent) + bgt.to_file(fname) + return gpd.read_file(fname) + + +def get_regis(): + fname = os.path.join(model_ws, "regis.nc") + if not os.path.isfile(fname): + if not os.path.isdir(model_ws): + os.makedirs(model_ws) + regis = nlmod.read.regis.get_regis(extent) + regis.to_netcdf(fname) + return xr.open_dataset(fname) + + +def get_structured_model_ds(): + model_ws = os.path.join(tempfile.gettempdir(), "test_grid_structured") + fname = os.path.join(model_ws, "ds.nc") + if not os.path.isfile(fname): + if not os.path.isdir(model_ws): + os.makedirs(model_ws) + ds = nlmod.get_ds(extent, model_name="test_grid", model_ws=model_ws) + ds.to_netcdf(fname) + return xr.open_dataset(fname) + + +def get_structured_model_ds_rotated(): + model_ws = os.path.join(tempfile.gettempdir(), "test_grid_structured_rotated") + fname = os.path.join(model_ws, "ds.nc") + if not os.path.isfile(fname): + if not os.path.isdir(model_ws): + os.makedirs(model_ws) + ds = nlmod.get_ds(extent, model_name="test_grid", model_ws=model_ws, angrot=15) + ds.to_netcdf(fname) + return xr.open_dataset(fname) + + +def get_vertex_model_ds(bgt=None): + model_ws = os.path.join(tempfile.gettempdir(), "test_grid_vertex") + fname = os.path.join(model_ws, "ds.nc") + if not os.path.isfile(fname): + if not os.path.isdir(model_ws): + os.makedirs(model_ws) + ds = get_structured_model_ds() + if bgt is None: + bgt = get_bgt() + ds = nlmod.grid.refine(ds, model_ws=model_ws, refinement_features=[(bgt, 1)]) + ds.to_netcdf(fname) + return xr.open_dataset(fname) + + +def get_vertex_model_ds_rotated(bgt=None): + model_ws = os.path.join(tempfile.gettempdir(), "test_grid_vertex_rotated") + fname = os.path.join(model_ws, "ds.nc") + if not os.path.isfile(fname): + if not os.path.isdir(model_ws): + os.makedirs(model_ws) + ds = get_structured_model_ds_rotated() + if bgt is None: + bgt = get_bgt() + ds = nlmod.grid.refine(ds, model_ws=model_ws, refinement_features=[(bgt, 1)]) + ds.to_netcdf(fname) + return xr.open_dataset(fname) + + +def test_get_ds_rotated(): + ds0 = get_structured_model_ds_rotated() + assert ds0.extent[0] == 0 and ds0.extent[2] == 0 + assert ds0.xorigin == extent[0] and ds0.yorigin == extent[2] + + # test refine method, by refining in all cells that contain surface water polygons + ds = get_vertex_model_ds_rotated() + assert len(ds.area) > np.prod(ds0.area.shape) + assert ds.extent[0] == 0 and ds.extent[2] == 0 + assert ds.xorigin == extent[0] and ds.yorigin == extent[2] + + f0, ax0 = plt.subplots() + nlmod.plot.modelgrid(ds0, ax=ax0) + f, ax = plt.subplots() + nlmod.plot.modelgrid(ds, ax=ax) + assert (np.array(ax.axis()) == np.array(ax0.axis())).all() + + +def test_vertex_da_to_ds(): + # for a normal grid + ds0 = get_structured_model_ds() + ds = get_vertex_model_ds() + da = nlmod.resample.vertex_da_to_ds(ds["top"], ds0, method="linear") + assert not da.isnull().all() + da = nlmod.resample.vertex_da_to_ds(ds["botm"], ds0, method="linear") + assert not da.isnull().all() + + # for a rotated grid + ds0 = get_structured_model_ds_rotated() + ds = get_vertex_model_ds_rotated() + da = nlmod.resample.vertex_da_to_ds(ds["top"], ds0, method="linear") + assert not da.isnull().all() + da = nlmod.resample.vertex_da_to_ds(ds["botm"], ds0, method="linear") + assert not da.isnull().all() + + +def test_fillnan_da(): + # for a structured grid + ds = get_structured_model_ds() + ds["top"][5, 5] = np.NaN + top = nlmod.resample.fillnan_da(ds["top"], ds=ds) + assert not np.isnan(top[5, 5]) + + # also for a vertex grid + ds = get_vertex_model_ds() + ds["top"][100] = np.NaN + mask = ds["top"].isnull() + assert mask.any() + top = nlmod.resample.fillnan_da(ds["top"], ds=ds) + assert not top[mask].isnull().any() + + +def test_gdf_to_bool_da(): + bgt = get_bgt() + + # test for a structured grid + ds = get_structured_model_ds() + da = nlmod.grid.gdf_to_bool_da(bgt, ds) + assert da.any() + + # test for a vertex grid + ds = get_vertex_model_ds() + da = nlmod.grid.gdf_to_bool_da(bgt, ds) + assert da.any() + + # tets for a slightly rotated structured grid + ds = get_structured_model_ds_rotated() + da = nlmod.grid.gdf_to_bool_da(bgt, ds) + assert da.any() + + # test for a rotated vertex grid + ds = get_vertex_model_ds_rotated() + da = nlmod.grid.gdf_to_bool_da(bgt, ds) + assert da.any() + + +def test_gdf_to_da(): + bgt = get_bgt() + + # test for a structured grid + ds = get_structured_model_ds() + da = nlmod.grid.gdf_to_da(bgt, ds, "relatieveHoogteligging", agg_method="max_area") + assert not da.isnull().all() + + # test for a vertex grid + ds = get_vertex_model_ds() + da = nlmod.grid.gdf_to_da(bgt, ds, "relatieveHoogteligging", agg_method="max_area") + assert not da.isnull().all() + + # tets for a slightly rotated structured grid + ds = get_structured_model_ds_rotated() + da = nlmod.grid.gdf_to_da(bgt, ds, "relatieveHoogteligging", agg_method="max_area") + assert not da.isnull().all() + + # test for a rotated vertex grid + ds = get_vertex_model_ds_rotated() + da = nlmod.grid.gdf_to_da(bgt, ds, "relatieveHoogteligging", agg_method="max_area") + assert not da.isnull().all() + + +def test_update_ds_from_layer_ds(): + bgt = get_bgt() + regis = get_regis() + + # test for a structured grid + ds = nlmod.get_ds(extent, delr=200) + ds = nlmod.grid.update_ds_from_layer_ds(ds, regis, method="nearest") + assert len(np.unique(ds["top"])) > 1 + ds = nlmod.grid.update_ds_from_layer_ds(ds, regis, method="average") + assert len(np.unique(ds["top"])) > 1 + + # test for a vertex grid + model_ws = os.path.join(tempfile.gettempdir(), "test_grid_vertex_200") + ds = nlmod.grid.refine(ds, model_ws=model_ws, refinement_features=[(bgt, 1)]) + ds = nlmod.grid.update_ds_from_layer_ds(ds, regis, method="nearest") + assert len(np.unique(ds["top"])) > 1 + ds = nlmod.grid.update_ds_from_layer_ds(ds, regis, method="average") + assert len(np.unique(ds["top"])) > 1 + + # tets for a slightly rotated structured grid + ds = nlmod.get_ds(extent, delr=200, angrot=15) + ds = nlmod.grid.update_ds_from_layer_ds(ds, regis, method="nearest") + assert len(np.unique(ds["top"])) > 1 + ds = nlmod.grid.update_ds_from_layer_ds(ds, regis, method="average") + assert len(np.unique(ds["top"])) > 1 + + # test for a rotated vertex grid + model_ws = os.path.join(tempfile.gettempdir(), "test_grid_vertex_200_rotated") + ds = nlmod.grid.refine(ds, model_ws=model_ws, refinement_features=[(bgt, 2)]) + ds = nlmod.grid.update_ds_from_layer_ds(ds, regis, method="nearest") + assert len(np.unique(ds["top"])) > 1 + ds = nlmod.grid.update_ds_from_layer_ds(ds, regis, method="average") + assert len(np.unique(ds["top"])) > 1 From 72322347f802ee88469e04cac43167c42d28c733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 6 Mar 2024 08:48:56 +0100 Subject: [PATCH 11/85] Add get_flow_residuals and get_flow_lower_face (#327) * Add get_flow_residuals and get_flow_lower_face * Minor changes Fix lays parameter for structured grids * Fix failing tests * Fix last failing test * Improve nlmod.gwf.output.get_budget_da() So it also works for multiple packages of the same type (for example 3 DRN-packages), by summing the budgets * Handle comments from @OnnoEbbens and replace grbfile by grb_file Like in flopy --- nlmod/gwf/output.py | 190 ++++++++++++++++++++++++++++++++--- nlmod/mfoutput/binaryfile.py | 15 ++- nlmod/mfoutput/mfoutput.py | 23 +++-- tests/test_015_gwf_output.py | 48 ++++++--- 4 files changed, 234 insertions(+), 42 deletions(-) diff --git a/nlmod/gwf/output.py b/nlmod/gwf/output.py index ed4ed748..4b4e4ea2 100644 --- a/nlmod/gwf/output.py +++ b/nlmod/gwf/output.py @@ -8,17 +8,19 @@ from shapely.geometry import Point from ..dims.grid import modelgrid_from_ds +from ..dims.resample import get_affine_world_to_mod from ..mfoutput.mfoutput import ( _get_budget_da, _get_heads_da, _get_time_index, _get_flopy_data_object, + _get_grb_file, ) logger = logging.getLogger(__name__) -def get_headfile(ds=None, gwf=None, fname=None, grbfile=None): +def get_headfile(ds=None, gwf=None, fname=None, grb_file=None): """Get flopy HeadFile object. Provide one of ds, gwf or fname. @@ -31,7 +33,7 @@ def get_headfile(ds=None, gwf=None, fname=None, grbfile=None): groundwater flow model, by default None fname : str, optional path to heads file, by default None - grbfile : str + grb_file : str path to file containing binary grid information Returns @@ -39,14 +41,14 @@ def get_headfile(ds=None, gwf=None, fname=None, grbfile=None): flopy.utils.HeadFile HeadFile object handle """ - return _get_flopy_data_object("head", ds, gwf, fname, grbfile) + return _get_flopy_data_object("head", ds, gwf, fname, grb_file) def get_heads_da( ds=None, gwf=None, fname=None, - grbfile=None, + grb_file=None, delayed=False, chunked=False, **kwargs, @@ -62,7 +64,7 @@ def get_heads_da( Flopy groundwaterflow object. fname : path, optional path to a binary heads file - grbfile : str, optional + grb_file : str, optional path to file containing binary grid information, only needed if reading output from file using fname delayed : bool, optional @@ -75,7 +77,7 @@ def get_heads_da( da : xarray.DataArray heads data array. """ - hobj = get_headfile(ds=ds, gwf=gwf, fname=fname, grbfile=grbfile) + hobj = get_headfile(ds=ds, gwf=gwf, fname=fname, grb_file=grb_file) # gwf.output.head() defaults to a structured grid if gwf is not None and ds is None and fname is None: kwargs["modelgrid"] = gwf.modelgrid @@ -99,7 +101,7 @@ def get_heads_da( return da -def get_cellbudgetfile(ds=None, gwf=None, fname=None, grbfile=None): +def get_cellbudgetfile(ds=None, gwf=None, fname=None, grb_file=None): """Get flopy CellBudgetFile object. Provide one of ds, gwf or fname. @@ -111,9 +113,9 @@ def get_cellbudgetfile(ds=None, gwf=None, fname=None, grbfile=None): gwf : flopy.mf6.ModflowGwf, optional groundwater flow model, by default None fname_cbc : str, optional - path to cell budget file, by default None\ - grbfile : str, optional - path to file containing binary grid information, only needed if + path to cell budget file, by default None + grb_file : str, optional + path to file containing binary grid information, only needed if fname_cbc is passed as only argument. Returns @@ -121,7 +123,7 @@ def get_cellbudgetfile(ds=None, gwf=None, fname=None, grbfile=None): flopy.utils.CellBudgetFile CellBudgetFile object handle """ - return _get_flopy_data_object("budget", ds, gwf, fname, grbfile) + return _get_flopy_data_object("budget", ds, gwf, fname, grb_file) def get_budget_da( @@ -129,7 +131,7 @@ def get_budget_da( ds=None, gwf=None, fname=None, - grbfile=None, + grb_file=None, column="q", delayed=False, chunked=False, @@ -148,7 +150,7 @@ def get_budget_da( fname : path, optional specify the budget file to load, if not provided budget file will be obtained from ds or gwf. - grbfile : str + grb_file : str path to file containing binary grid information, only needed if reading output from file using fname column : str @@ -164,7 +166,7 @@ def get_budget_da( da : xarray.DataArray budget data array. """ - cbcobj = get_cellbudgetfile(ds=ds, gwf=gwf, fname=fname, grbfile=grbfile) + cbcobj = get_cellbudgetfile(ds=ds, gwf=gwf, fname=fname, grb_file=grb_file) da = _get_budget_da(cbcobj, text, column=column, **kwargs) da.attrs["units"] = "m3/d" @@ -232,7 +234,157 @@ def get_gwl_from_wet_cells(head, layer="layer", botm=None): return gwl -def get_head_at_point(head, x, y, ds=None, gi=None, drop_nan_layers=True): +def get_flow_residuals(ds, gwf=None, fname=None, grb_file=None, kstpkper=None): + """ + Get the flow residuals of a MODFLOW 6 simulation. + + Parameters + ---------- + ds : xarray.Dataset + Xarray dataset with model data. + gwf : flopy ModflowGwf, optional + Flopy groundwaterflow object. One of ds or gwf must be provided. + fname : path, optional + specify the budget file to load, if not provided budget file will + be obtained from ds or gwf. + grb_file : str + The location of the grb-file. grb_file is determied from ds when None. The + default is None. + kstpkper : tuple of 2 ints, optional + The index of the timestep and the stress period to include in the result. Include + all data in the budget-file when None. The default is None. + + Returns + ------- + da : xr.DataArray + The flow residual in each cell, in m3/d. + + """ + if grb_file is None: + grb_file = _get_grb_file(ds) + grb = flopy.mf6.utils.MfGrdFile(grb_file) + cbf = get_cellbudgetfile(ds=ds, gwf=gwf, fname=fname, grb_file=grb_file) + dims = ds["botm"].dims + coords = ds["botm"].coords + flowja = cbf.get_data(text="FLOW-JA-FACE", kstpkper=kstpkper) + mask_active = np.diff(grb.ia) > 0 + flowja_index = grb.ia[:-1][mask_active] + if kstpkper is None: + # loop over all timesteps/stress-periods + residuals = [] + for iflowja in flowja: + # residuals.append(flopy.mf6.utils.get_residuals(iflowja, grb_file)) + # use our own faster method instead of a for loop: + residual = np.full(grb.shape, np.NaN) + residual.ravel()[mask_active] = iflowja.flatten()[flowja_index] + residuals.append(residual) + dims = ("time",) + dims + coords = dict(coords) | {"time": _get_time_index(cbf, ds)} + else: + # residuals = flopy.mf6.utils.get_residuals(flowja[0], grb_file) + # use our own faster method instead of a for loop: + residuals = np.full(grb.shape, np.NaN) + residuals.ravel()[mask_active] = flowja[0].flatten()[flowja_index] + da = xr.DataArray(residuals, dims=dims, coords=coords) + return da + + +def get_flow_lower_face( + ds, gwf=None, fname=None, grb_file=None, kstpkper=None, lays=None +): + """ + Get the flow over the lower face of all model cells + + The flow Lower Face (flf) used to be written to the budget file in previous versions + of MODFLOW. In MODFLOW 6 we determine these flows from the flow-ja-face-records. + + Parameters + ---------- + ds : xarray.Dataset + Xarray dataset with model data. + gwf : flopy ModflowGwf, optional + Flopy groundwaterflow object. One of ds or gwf must be provided. + fname : path, optional + specify the budget file to load, if not provided budget file will + be obtained from ds or gwf. + grb_file : str, optional + The location of the grb-file. grb_file is determied from ds when None. The + default is None. + kstpkper : tuple of 2 ints, optional + The index of the timestep and the stress period to include in the result. Include + all data in the budget-file when None. The default is None. + lays : int or list of ints, optional + The layers to include in the result. When lays is None, all layers are included. + The default is None. + + Returns + ------- + da : xr.DataArray + The flow over the lower face of each cell, in m3/d. + + """ + if grb_file is None: + grb_file = _get_grb_file(ds) + cbf = get_cellbudgetfile(ds=ds, gwf=gwf, fname=fname, grb_file=grb_file) + flowja = cbf.get_data(text="FLOW-JA-FACE", kstpkper=kstpkper) + + if ds.gridtype == "vertex": + # determine flf_index first + grb = flopy.mf6.utils.MfGrdFile(grb_file) + + if lays is None: + lays = range(grb.nlay) + if isinstance(lays, int): + lays = [lays] + shape = (len(lays), len(ds.icell2d)) + + flf_index = np.full(shape, -1) + # get these properties outside of the for loop to increase speed + grb_ia = grb.ia + grb_ja = grb.ja + for ilay, lay in enumerate(lays): + ja_start_next_layer = (lay + 1) * grb.ncpl + for icell2d in range(grb.ncpl): + node = lay * grb.ncpl + icell2d + ia = np.arange(grb_ia[node], grb_ia[node + 1]) + mask = grb_ja[ia] >= ja_start_next_layer + if mask.any(): + # assert mask.sum() == 1 + flf_index[ilay, icell2d] = int(ia[mask]) + coords = ds["botm"][lays].coords + else: + coords = ds["botm"].coords + dims = ds["botm"].dims + + if kstpkper is None: + # loop over all tiesteps/stress-periods + flfs = [] + for iflowja in flowja: + if ds.gridtype == "vertex": + flf = np.full(shape, np.NaN) + mask = flf_index >= 0 + flf[mask] = iflowja[0, 0, flf_index[mask]] + else: + _, _, flf = flopy.mf6.utils.get_structured_faceflows(iflowja, grb_file) + flfs.append(flf) + dims = ("time",) + dims + coords = dict(coords) | {"time": _get_time_index(cbf, ds)} + else: + if ds.gridtype == "vertex": + flfs = np.full(shape, np.NaN) + mask = flf_index >= 0 + flfs[mask] = flowja[0][0, 0, flf_index[mask]] + else: + _, _, flfs = flopy.mf6.utils.get_structured_faceflows(flowja[0], grb_file) + da = xr.DataArray(flfs, dims=dims, coords=coords) + if ds.gridtype != "vertex" and lays is not None: + da = da.isel(layer=lays) + return da + + +def get_head_at_point( + head, x, y, ds=None, gi=None, drop_nan_layers=True, rotated=False +): """Get the head at a certain point from a head DataArray for all cells. Parameters @@ -253,12 +405,20 @@ def get_head_at_point(head, x, y, ds=None, gi=None, drop_nan_layers=True): None. drop_nan_layers : bool, optional Drop layers that are NaN at all timesteps. The default is True. + rotated : bool, optional + If the model grid has a rotation, and rotated is False, x and y are in model + coordinates. Otherwise x and y are in real world coordinates. The defaults is + False. Returns ------- head_point : xarray.DataArray A DataArray with dimensions (time, layer). """ + if rotated and "angrot" in ds.attrs and ds.attrs["angrot"] != 0.0: + # calculate model coordinates from the specified real-world coordinates + x, y = get_affine_world_to_mod(ds) * (x, y) + if "icell2d" in head.dims: if gi is None: if ds is None: diff --git a/nlmod/mfoutput/binaryfile.py b/nlmod/mfoutput/binaryfile.py index a3197f0c..9d228a1b 100644 --- a/nlmod/mfoutput/binaryfile.py +++ b/nlmod/mfoutput/binaryfile.py @@ -138,13 +138,24 @@ def _get_binary_budget_data(kstpkper, fobj, text, column="q"): idx = np.array([idx]) header = fobj.recordarray[idx] - ipos = fobj.iposarray[idx].item() - imeth = header["imeth"][0] t = header["text"][0] if isinstance(t, bytes): t = t.decode("utf-8") + data = [] + for ipos in fobj.iposarray[idx]: + data.append(_get_binary_budget_record(fobj, ipos, header, column)) + + if len(data) == 1: + return data[0] + else: + return np.ma.sum(data, axis=0) + + +def _get_binary_budget_record(fobj, ipos, header, column): + """Get a single data record from the budget file.""" + imeth = header["imeth"][0] nlay = abs(header["nlay"][0]) nrow = header["nrow"][0] ncol = header["ncol"][0] diff --git a/nlmod/mfoutput/mfoutput.py b/nlmod/mfoutput/mfoutput.py index 64158e3d..d26fc73c 100644 --- a/nlmod/mfoutput/mfoutput.py +++ b/nlmod/mfoutput/mfoutput.py @@ -239,7 +239,7 @@ def _get_budget_da( return da -def _get_flopy_data_object(var, ds=None, gwml=None, fname=None, grbfile=None): +def _get_flopy_data_object(var, ds=None, gwml=None, fname=None, grb_file=None): """Get modflow HeadFile or CellBudgetFile object, containg heads, budgets or concentrations @@ -255,7 +255,7 @@ def _get_flopy_data_object(var, ds=None, gwml=None, fname=None, grbfile=None): groundwater flow or transport model, by default None fname : str, optional path to Head- or CellBudgetFile, by default None - grbfile : str, optional + grb_file : str, optional path to file containing binary grid information, if None modelgrid information is obtained from ds. By default None @@ -283,14 +283,11 @@ def _get_flopy_data_object(var, ds=None, gwml=None, fname=None, grbfile=None): # return gwf.output.head(), gwf.output.budget() or gwt.output.concentration() return getattr(gwml.output, var)() fname = os.path.join(ds.model_ws, ds.model_name + extension) - if grbfile is None and ds is not None: + if grb_file is None and ds is not None: # get grb file - if ds.gridtype == "vertex": - grbfile = os.path.join(ds.model_ws, ds.model_name + ".disv.grb") - elif ds.gridtype == "structured": - grbfile = os.path.join(ds.model_ws, ds.model_name + ".dis.grb") - if grbfile is not None and os.path.exists(grbfile): - modelgrid = flopy.mf6.utils.MfGrdFile(grbfile).modelgrid + grb_file = _get_grb_file(ds) + if grb_file is not None and os.path.exists(grb_file): + modelgrid = flopy.mf6.utils.MfGrdFile(grb_file).modelgrid elif ds is not None: modelgrid = modelgrid_from_ds(ds) else: @@ -307,3 +304,11 @@ def _get_flopy_data_object(var, ds=None, gwml=None, fname=None, grbfile=None): logger.warning(msg) warnings.warn(msg) return flopy.utils.HeadFile(fname, text=var, modelgrid=modelgrid) + + +def _get_grb_file(ds): + if ds.gridtype == "vertex": + grb_file = os.path.join(ds.model_ws, ds.model_name + ".disv.grb") + elif ds.gridtype == "structured": + grb_file = os.path.join(ds.model_ws, ds.model_name + ".dis.grb") + return grb_file diff --git a/tests/test_015_gwf_output.py b/tests/test_015_gwf_output.py index 074e920b..fc3608ee 100644 --- a/tests/test_015_gwf_output.py +++ b/tests/test_015_gwf_output.py @@ -67,17 +67,17 @@ def test_create_small_model_grid_only(tmpdir, model_name="test"): assert np.array_equal(da.values, heads_correct, equal_nan=True) fname_hds = os.path.join(ds.model_ws, ds.model_name + ".hds") - grbfile = os.path.join(ds.model_ws, ds.model_name + ".dis.grb") - da = get_heads_da(ds=None, gwf=None, fname=fname_hds, grbfile=grbfile) # fname + grb_file = os.path.join(ds.model_ws, ds.model_name + ".dis.grb") + da = get_heads_da(ds=None, gwf=None, fname=fname_hds, grb_file=grb_file) # fname assert np.array_equal(da.values, heads_correct, equal_nan=True) # budget da = get_budget_da("CHD", ds=ds, gwf=None, fname=None) # ds da = get_budget_da("CHD", ds=None, gwf=gwf, fname=None) # gwf fname_cbc = os.path.join(ds.model_ws, ds.model_name + ".cbc") - get_budget_da("CHD", ds=None, gwf=None, fname=fname_cbc, grbfile=grbfile) # fname + get_budget_da("CHD", ds=None, gwf=None, fname=fname_cbc, grb_file=grb_file) # fname get_budget_da( - "DATA-SPDIS", column="qz", ds=None, gwf=None, fname=fname_cbc, grbfile=grbfile + "DATA-SPDIS", column="qz", ds=None, gwf=None, fname=fname_cbc, grb_file=grb_file ) # fname # unstructured @@ -127,18 +127,18 @@ def test_create_small_model_grid_only(tmpdir, model_name="test"): assert np.array_equal(da.values, heads_correct, equal_nan=True) fname_hds = os.path.join(ds.model_ws, ds.model_name + ".hds") - grbfile = os.path.join(ds.model_ws, ds.model_name + ".disv.grb") - da = get_heads_da(ds=None, gwf=None, fname=fname_hds, grbfile=grbfile) # fname + grb_file = os.path.join(ds.model_ws, ds.model_name + ".disv.grb") + da = get_heads_da(ds=None, gwf=None, fname=fname_hds, grb_file=grb_file) # fname assert np.array_equal(da.values, heads_correct, equal_nan=True) # budget da = get_budget_da("CHD", ds=ds_unstr, gwf=None, fname=None) # ds da = get_budget_da("CHD", ds=None, gwf=gwf_unstr, fname=None) # gwf da = get_budget_da( - "CHD", ds=None, gwf=None, fname=fname_cbc, grbfile=grbfile + "CHD", ds=None, gwf=None, fname=fname_cbc, grb_file=grb_file ) # fname _ = get_budget_da( - "DATA-SPDIS", column="qz", ds=None, gwf=None, fname=fname_cbc, grbfile=grbfile + "DATA-SPDIS", column="qz", ds=None, gwf=None, fname=fname_cbc, grb_file=grb_file ) # fname @@ -150,8 +150,8 @@ def test_get_heads_da_from_file_structured_no_grb(): def test_get_heads_da_from_file_structured_with_grb(): fname_hds = "./tests/data/mf6output/structured/test.hds" - grbfile = "./tests/data/mf6output/structured/test.dis.grb" - nlmod.gwf.output.get_heads_da(fname=fname_hds, grbfile=grbfile) + grb_file = "./tests/data/mf6output/structured/test.dis.grb" + nlmod.gwf.output.get_heads_da(fname=fname_hds, grb_file=grb_file) def test_get_budget_da_from_file_structured_no_grb(): @@ -162,8 +162,8 @@ def test_get_budget_da_from_file_structured_no_grb(): def test_get_budget_da_from_file_structured_with_grb(): fname_cbc = "./tests/data/mf6output/structured/test.cbc" - grbfile = "./tests/data/mf6output/structured/test.dis.grb" - nlmod.gwf.output.get_budget_da("CHD", fname=fname_cbc, grbfile=grbfile) + grb_file = "./tests/data/mf6output/structured/test.dis.grb" + nlmod.gwf.output.get_budget_da("CHD", fname=fname_cbc, grb_file=grb_file) def test_get_heads_da_from_file_vertex_no_grb(): @@ -174,8 +174,8 @@ def test_get_heads_da_from_file_vertex_no_grb(): def test_get_heads_da_from_file_vertex_with_grb(): fname_hds = "./tests/data/mf6output/vertex/test.hds" - grbfile = "./tests/data/mf6output/vertex/test.disv.grb" - nlmod.gwf.output.get_heads_da(fname=fname_hds, grbfile=grbfile) + grb_file = "./tests/data/mf6output/vertex/test.disv.grb" + nlmod.gwf.output.get_heads_da(fname=fname_hds, grb_file=grb_file) def test_get_budget_da_from_file_vertex_no_grb(): @@ -186,8 +186,8 @@ def test_get_budget_da_from_file_vertex_no_grb(): def test_get_budget_da_from_file_vertex_with_grb(): fname_cbc = "./tests/data/mf6output/vertex/test.cbc" - grbfile = "./tests/data/mf6output/vertex/test.disv.grb" - nlmod.gwf.output.get_budget_da("CHD", fname=fname_cbc, grbfile=grbfile) + grb_file = "./tests/data/mf6output/vertex/test.disv.grb" + nlmod.gwf.output.get_budget_da("CHD", fname=fname_cbc, grb_file=grb_file) def test_postprocess_head(): @@ -199,3 +199,19 @@ def test_postprocess_head(): nlmod.gwf.get_gwl_from_wet_cells(head, botm=ds["botm"]) nlmod.gwf.get_head_at_point(head, float(ds.x.mean()), float(ds.y.mean()), ds=ds) + + +def test_get_flow_residuals(): + ds = test_001_model.get_ds_from_cache("basic_sea_model") + da = nlmod.gwf.output.get_flow_residuals(ds) + assert "time" in da.dims + da = nlmod.gwf.output.get_flow_residuals(ds, kstpkper=(0, 0)) + assert "time" not in da.dims + + +def test_get_flow_lower_face(): + ds = test_001_model.get_ds_from_cache("basic_sea_model") + da = nlmod.gwf.output.get_flow_lower_face(ds) + assert "time" in da.dims + da = nlmod.gwf.output.get_flow_lower_face(ds, kstpkper=(0, 0)) + assert "time" not in da.dims From 464edbebd345e4db9e92e89ae96f8a31d4865b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 6 Mar 2024 10:43:54 +0100 Subject: [PATCH 12/85] Add precision to output-files --- nlmod/gwf/output.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/nlmod/gwf/output.py b/nlmod/gwf/output.py index 4b4e4ea2..422f1db3 100644 --- a/nlmod/gwf/output.py +++ b/nlmod/gwf/output.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) -def get_headfile(ds=None, gwf=None, fname=None, grb_file=None): +def get_headfile(ds=None, gwf=None, fname=None, grb_file=None, **kwargs): """Get flopy HeadFile object. Provide one of ds, gwf or fname. @@ -41,7 +41,7 @@ def get_headfile(ds=None, gwf=None, fname=None, grb_file=None): flopy.utils.HeadFile HeadFile object handle """ - return _get_flopy_data_object("head", ds, gwf, fname, grb_file) + return _get_flopy_data_object("head", ds, gwf, fname, grb_file, **kwargs) def get_heads_da( @@ -51,6 +51,7 @@ def get_heads_da( grb_file=None, delayed=False, chunked=False, + precision="auto", **kwargs, ): """Read binary heads file. @@ -71,13 +72,19 @@ def get_heads_da( if delayed is True, do not load output data into memory, default is False. chunked : bool, optional chunk data array containing output, default is False. + precision : str, optional + precision of floating point data in the head-file. Accepted values are 'auto', + 'single' or 'double'. When precision is 'auto', it is determined from the + head-file. Default is 'auto'. Returns ------- da : xarray.DataArray heads data array. """ - hobj = get_headfile(ds=ds, gwf=gwf, fname=fname, grb_file=grb_file) + hobj = get_headfile( + ds=ds, gwf=gwf, fname=fname, grb_file=grb_file, precision=precision + ) # gwf.output.head() defaults to a structured grid if gwf is not None and ds is None and fname is None: kwargs["modelgrid"] = gwf.modelgrid @@ -101,7 +108,7 @@ def get_heads_da( return da -def get_cellbudgetfile(ds=None, gwf=None, fname=None, grb_file=None): +def get_cellbudgetfile(ds=None, gwf=None, fname=None, grb_file=None, **kwargs): """Get flopy CellBudgetFile object. Provide one of ds, gwf or fname. @@ -123,7 +130,7 @@ def get_cellbudgetfile(ds=None, gwf=None, fname=None, grb_file=None): flopy.utils.CellBudgetFile CellBudgetFile object handle """ - return _get_flopy_data_object("budget", ds, gwf, fname, grb_file) + return _get_flopy_data_object("budget", ds, gwf, fname, grb_file, **kwargs) def get_budget_da( @@ -135,6 +142,7 @@ def get_budget_da( column="q", delayed=False, chunked=False, + precision="auto", **kwargs, ): """Read binary budget file. @@ -160,13 +168,19 @@ def get_budget_da( if delayed is True, do not load output data into memory, default is False. chunked : bool, optional chunk data array containing output, default is False. + precision : str, optional + precision of floating point data in the budget-file. Accepted values are 'auto', + 'single' or 'double'. When precision is 'auto', it is determined from the + budget-file. Default is 'auto'. Returns ------- da : xarray.DataArray budget data array. """ - cbcobj = get_cellbudgetfile(ds=ds, gwf=gwf, fname=fname, grb_file=grb_file) + cbcobj = get_cellbudgetfile( + ds=ds, gwf=gwf, fname=fname, grb_file=grb_file, precision=precision + ) da = _get_budget_da(cbcobj, text, column=column, **kwargs) da.attrs["units"] = "m3/d" From 007d4c95fa4370c770a1225ca947f06e8e18393b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 6 Mar 2024 10:59:52 +0100 Subject: [PATCH 13/85] Add krags to _get_flopy_data_object --- nlmod/mfoutput/mfoutput.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nlmod/mfoutput/mfoutput.py b/nlmod/mfoutput/mfoutput.py index d26fc73c..14870372 100644 --- a/nlmod/mfoutput/mfoutput.py +++ b/nlmod/mfoutput/mfoutput.py @@ -239,7 +239,9 @@ def _get_budget_da( return da -def _get_flopy_data_object(var, ds=None, gwml=None, fname=None, grb_file=None): +def _get_flopy_data_object( + var, ds=None, gwml=None, fname=None, grb_file=None, **kwargs +): """Get modflow HeadFile or CellBudgetFile object, containg heads, budgets or concentrations @@ -298,12 +300,12 @@ def _get_flopy_data_object(var, ds=None, gwml=None, fname=None, grb_file=None): if modelgrid is None: logger.error(msg) raise ValueError(msg) - return flopy.utils.CellBudgetFile(fname, modelgrid=modelgrid) + return flopy.utils.CellBudgetFile(fname, modelgrid=modelgrid, **kwargs) else: if modelgrid is None: logger.warning(msg) warnings.warn(msg) - return flopy.utils.HeadFile(fname, text=var, modelgrid=modelgrid) + return flopy.utils.HeadFile(fname, text=var, modelgrid=modelgrid, **kwargs) def _get_grb_file(ds): From f8046a9fefd7381ce5f8a49d856ce7f67f0624a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Tue, 12 Mar 2024 18:56:06 +0100 Subject: [PATCH 14/85] Improve export to ugrid_nc-file, to support imod qgis plugin (#329) * Improve export to ugrid_nc-file, to support imod qgis plugin * Improve docstring --- nlmod/gis.py | 55 ++++++++++++++++++++++++++++++++++++------- tests/test_014_gis.py | 6 ++++- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/nlmod/gis.py b/nlmod/gis.py index d9ee9ce3..17d4709c 100644 --- a/nlmod/gis.py +++ b/nlmod/gis.py @@ -6,6 +6,7 @@ from .dims.grid import polygons_from_model_ds from .dims.resample import get_affine_mod_to_world +from .dims.layers import calculate_thickness logger = logging.getLogger(__name__) @@ -308,6 +309,9 @@ def ds_to_ugrid_nc_file( xv="xv", yv="yv", face_node_connectivity="icvert", + split_layer_dimension=True, + split_time_dimension=False, + for_imod_qgis_plugin=False, ): """Save a model dataset to a UGRID NetCDF file, so it can be opened as a Mesh Layer in qgis. @@ -335,13 +339,24 @@ def ds_to_ugrid_nc_file( face_node_connectivity : str, optional The name of the variable that contains the indexes of the vertices for each face. The default is 'icvert'. + split_layer_dimension : bool, optional + Splits the layer dimension into seperate variables when True. The defaults is + True. + split_time_dimension : bool, optional + Splits the time dimension into seperate variables when True. The defaults is + False. + for_imod_qgis_plugin : bool, optional + When True, set some properties of the netcdf file to improve compatibility with + the iMOD-QGIS plugin. Layers are renamed to 'layer_i' until 'layer_n', a + variable 'top' is added for each layer, and the variable 'botm' is renamed to + 'bottom'. The default is False. Returns ------- ds : xr.DataSet The dataset that was saved to a NetCDF-file. Can be used for debugging. """ - assert model_ds.gridtype == "vertex", "Only vertex grids are supported" + assert model_ds.gridtype == "vertex", "Only vertex grids are supported for now" # copy the dataset, so we do not alter the original one ds = model_ds.copy() @@ -377,6 +392,10 @@ def ds_to_ugrid_nc_file( ds[face_node_connectivity].attrs["cf_role"] = "face_node_connectivity" ds[face_node_connectivity].attrs["start_index"] = 0 + if for_imod_qgis_plugin and "botm" in ds: + ds["top"] = ds["botm"] + calculate_thickness(ds) + ds = ds.rename({"botm": "bottom"}) + # set for each of the variables that they describe the faces if variables is None: variables = list(ds.keys()) @@ -405,9 +424,16 @@ def ds_to_ugrid_nc_file( ds[var].encoding["dtype"] = np.int32 # Breaks down variables with a layer dimension into separate variables. - ds, variables = _break_down_dimension(ds, variables, "layer") - # Breaks down variables with a time dimension into separate variables. - ds, variables = _break_down_dimension(ds, variables, "time") + if split_layer_dimension: + if for_imod_qgis_plugin: + ds, variables = _break_down_dimension( + ds, variables, "layer", add_dim_name=True, add_one_based_index=True + ) + else: + ds, variables = _break_down_dimension(ds, variables, "layer") + if split_time_dimension: + # Breaks down variables with a time dimension into separate variables. + ds, variables = _break_down_dimension(ds, variables, "time") # only keep the selected variables ds = ds[variables + [dummy_var, xv, yv, face_node_connectivity]] @@ -417,14 +443,27 @@ def ds_to_ugrid_nc_file( return ds -def _break_down_dimension(ds, variables, dim): - # Copied and altered from imod-python. +def _break_down_dimension( + ds, variables, dim, add_dim_name=False, add_one_based_index=False +): + """Internal method to split a dimension of a variable into multiple variables. + + Copied and altered from imod-python. + """ + keep_vars = [] for var in variables: if dim in ds[var].dims: stacked = ds[var] - for value in stacked[dim].values: - name = f"{var}_{value}" + for i, value in enumerate(stacked[dim].values): + name = var + if add_dim_name: + name = f"{name}_{dim}" + if add_one_based_index: + name = f"{name}_{i+1}" + else: + name = f"{name}_{value}" + ds[name] = stacked.sel({dim: value}, drop=True) if "long_name" in ds[name].attrs: long_name = ds[name].attrs["long_name"] diff --git a/tests/test_014_gis.py b/tests/test_014_gis.py index b944a7b5..0ae4231f 100644 --- a/tests/test_014_gis.py +++ b/tests/test_014_gis.py @@ -17,4 +17,8 @@ def test_vertex_da_to_gdf(): def test_ds_to_ugrid_nc_file(): ds = util.get_ds_vertex() - nlmod.gis.ds_to_ugrid_nc_file(ds, os.path.join("data", "ugrid_test.nc")) + fname = os.path.join("data", "ugrid_test.nc") + nlmod.gis.ds_to_ugrid_nc_file(ds, fname) + + fname = os.path.join("data", "ugrid_test_qgis.nc") + nlmod.gis.ds_to_ugrid_nc_file(ds, fname, for_imod_qgis_plugin=True) From ebc07b0365896d4087784d0b47d921a1fed44a62 Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Wed, 13 Mar 2024 12:30:17 +0100 Subject: [PATCH 15/85] Update attributes_encodings.py (#332) Error in computing the add_offset --- nlmod/dims/attributes_encodings.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nlmod/dims/attributes_encodings.py b/nlmod/dims/attributes_encodings.py index 13512342..b93587fc 100644 --- a/nlmod/dims/attributes_encodings.py +++ b/nlmod/dims/attributes_encodings.py @@ -229,9 +229,13 @@ def get_encodings( def compute_scale_and_offset(min_value, max_value): """ - Computes the scale_factor and offset for the dataset using a min_value and max_value, - and int16. Useful for maximizing the compression of a dataset. - + Reduce precision of the dataset by storing it as int16 and thereby reducing the precision. + + Computes the scale_factor and offset for the dataset using a min_value and max_value to + transform the range of the dataset to the range of valid int16 values. The packed value + is computed as: + packed_value = (unpacked_value - add_offset) / scale_factor + Parameters ---------- min_value : float @@ -250,7 +254,7 @@ def compute_scale_and_offset(min_value, max_value): # from -32766 to 32767, because -32767 is the default fillvalue. width = 32766 + 32767 scale_factor = (max_value - min_value) / width - add_offset = 1.0 + scale_factor * (width - 1) / 2 + add_offset = max_value - scale_factor * 32767 return scale_factor, add_offset From f7a34dca78d99904daf23e3a40f158bf7d1bcde8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Fri, 15 Mar 2024 13:57:27 +0100 Subject: [PATCH 16/85] Add crs to get_gwo_wells --- nlmod/read/nhi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlmod/read/nhi.py b/nlmod/read/nhi.py index d58c1819..0ddaf34b 100644 --- a/nlmod/read/nhi.py +++ b/nlmod/read/nhi.py @@ -268,7 +268,7 @@ def get_gwo_wells( page = None df = pd.concat(properties) geometry = gpd.points_from_xy(df.XCoordinate, df.YCoordinate) - gdf = gpd.GeoDataFrame(df, geometry=geometry) + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs=28992) if well_index is not None: gdf = gdf.set_index(well_index) return gdf From 97182faad038fd3a5d52466c808cdad7be23de42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Fri, 15 Mar 2024 13:57:52 +0100 Subject: [PATCH 17/85] Allow a DataArray in polygon_to_hfb --- nlmod/gwf/horizontal_flow_barrier.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nlmod/gwf/horizontal_flow_barrier.py b/nlmod/gwf/horizontal_flow_barrier.py index 0c6cc07a..fe97f823 100644 --- a/nlmod/gwf/horizontal_flow_barrier.py +++ b/nlmod/gwf/horizontal_flow_barrier.py @@ -2,6 +2,7 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd +import xarray as xr from shapely.geometry import Point, Polygon from ..dims.grid import gdf_to_da, gdf_to_grid @@ -217,7 +218,9 @@ def line2hfb(gdf, gwf, prevent_rings=True, plot=False): def polygon_to_hfb( gdf, ds, column=None, gwf=None, lay=0, hydchr=1 / 100, add_data=False ): - if isinstance(gdf, str): + if isinstance(gdf, xr.DataArray): + da = gdf + elif isinstance(gdf, str): da = ds[gdf] else: if column is None: From 75632b2bb71169906d2d3d315a4e15613d66f8a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Fri, 15 Mar 2024 14:05:32 +0100 Subject: [PATCH 18/85] Add get_time_step_length --- nlmod/dims/time.py | 25 +++++++++++++++++++++++++ tests/test_016_time.py | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/nlmod/dims/time.py b/nlmod/dims/time.py index f766354a..f1b21c17 100644 --- a/nlmod/dims/time.py +++ b/nlmod/dims/time.py @@ -408,6 +408,31 @@ def estimate_nstp( return nstp_ceiled +def get_time_step_length(perlen, nstp, tsmult): + """ + Get the length of the timesteps within a singe stress-period. + + Parameters + ---------- + perlen : float + The length of the stress period, in the time unit of the model (generally days). + nstp : int + The numer of timesteps within the stress period. + tsmult : float + THe time step multiplier, generally equal or lager than 1. + + Returns + ------- + t : np.ndarray + An array with the length of each of the timesteps within the stress period, in + the same unit as perlen. + + """ + t = np.array([tsmult**x for x in range(nstp)]) + t = t * perlen / t.sum() + return t + + def ds_time_from_model(gwf): warnings.warn( "this function was renamed to `ds_time_idx_from_model`. " diff --git a/tests/test_016_time.py b/tests/test_016_time.py index 1e3339c5..056f7a75 100644 --- a/tests/test_016_time.py +++ b/tests/test_016_time.py @@ -28,3 +28,7 @@ def test_ds_time_from_tdis_settings(): elapsed = (tidx.to_numpy() - np.datetime64("2000")) / np.timedelta64(1, "D") assert np.allclose(elapsed, [100, 150, 200, 233.33333333, 300.0]) + + +def test_get_time_step_length(): + assert (nlmod.time.get_time_step_length(100, 2, 1.5) == np.array([40, 60])).all() From 7847d8bbb4fd0f1e7ba47735b0d5b237cb013d77 Mon Sep 17 00:00:00 2001 From: Martin Vonk Date: Tue, 2 Apr 2024 17:56:58 +0200 Subject: [PATCH 19/85] update knmi_data_platform base_url --- nlmod/read/knmi_data_platform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nlmod/read/knmi_data_platform.py b/nlmod/read/knmi_data_platform.py index f6efadf9..44c1b90c 100644 --- a/nlmod/read/knmi_data_platform.py +++ b/nlmod/read/knmi_data_platform.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) # base_url = "https://api.dataplatform.knmi.nl/dataset-content/v1/datasets" -base_url = "https://api.dataplatform.knmi.nl/open-data" +base_url = "https://api.dataplatform.knmi.nl/open-data/v1" def get_anonymous_api_key() -> Union[str, None]: @@ -69,6 +69,7 @@ def get_list_of_files( params = {"maxKeys": f"{max_keys}"} if start_after_filename is not None: params["startAfterFilename"] = start_after_filename + logger.debug(f"Request to {url=} with {params=}") r = requests.get( url, params=params, headers={"Authorization": api_key}, timeout=timeout ) From 45f09ec8d3975aeec85412fe3575975093b523d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Thu, 4 Apr 2024 13:48:56 +0200 Subject: [PATCH 20/85] Update webservices Amstel, Gooi en Vecht --- nlmod/read/waterboard.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/nlmod/read/waterboard.py b/nlmod/read/waterboard.py index 8abe21a0..bbf0779a 100644 --- a/nlmod/read/waterboard.py +++ b/nlmod/read/waterboard.py @@ -49,15 +49,16 @@ def get_configuration(): config["Amstel, Gooi en Vecht"] = { "bgt_code": "W0155", "watercourses": { - "url": "https://maps.waternet.nl/arcgis/rest/services/AGV_Legger/AGV_Onderh_Secundaire_Watergangen/MapServer", - "layer": 40, - "bottom_width": "BODEMBREEDTE", - "bottom_height": "BODEMHOOGTE", - "water_depth": "WATERDIEPTE", + "url": "https://maps.waternet.nl/arcgis/rest/services/Publiek/WNET_GEO_LEGGER_WL_2021/MapServer", + "layer": 0, # Primaire Waterloop Legger + "bottom_width": "AVVBODDR", + "bottom_height": "AVVBODH", + "water_depth": "AVVDIEPT", + "index": "OVKIDENT", }, "level_areas": { - "url": "https://maps.waternet.nl/arcgis/rest/services/AGV_Legger/Vastgestelde_Waterpeilen/MapServer", - "layer": 0, + "url": "https://maps.waternet.nl/arcgis/rest/services/Publiek/GW_GPG/MapServer", + "layer": 5, # Vigerende peilgebieden "index": "GPGIDENT", "summer_stage": [ "GPGZMRPL", From 5bae2b1beb9f7c401dbac6af8f1e3add6701745c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Fri, 5 Apr 2024 15:06:22 +0200 Subject: [PATCH 21/85] Update download_level_areas and download_watercourses so they work with just an extent --- nlmod/gwf/surface_water.py | 137 ++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 56 deletions(-) diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index 6e7bf0b3..07d7c60c 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -12,7 +12,7 @@ from ..dims.grid import gdf_to_grid from ..dims.layers import get_idomain -from ..dims.resample import get_extent_polygon +from ..dims.resample import get_extent_polygon, extent_to_polygon from ..read import bgt, waterboard from ..cache import cache_pickle @@ -571,15 +571,20 @@ def get_gdf_stage(gdf, season="winter"): def download_level_areas( - gdf, extent=None, config=None, raise_exceptions=True, drop_duplicates=True, **kwargs + gdf=None, + extent=None, + config=None, + raise_exceptions=True, + drop_duplicates=True, + **kwargs, ): """Download level areas (peilgebieden) of bronhouders. Parameters ---------- - gdf : geopandas.GeoDataFrame + gdf : geopandas.GeoDataFrame, optional A GeoDataFrame with surface water features, containing the column "bronhouder". - extent : list, tuple or np.array + extent : list, tuple or np.array, optional Model extent (xmin, xmax, ymin, ymax). When extent is None, all data of the water boards in gdf are downloaded downloaded. config : dict, optional @@ -602,55 +607,57 @@ def download_level_areas( """ if config is None: config = waterboard.get_configuration() - bronhouders = gdf["bronhouder"].unique() + wbs = _get_waterboard_selection(gdf=gdf, extent=extent, config=config) + la = {} data_kind = "level_areas" - for wb in config.keys(): - if config[wb]["bgt_code"] in bronhouders: - logger.info(f"Downloading {data_kind} for {wb}") - try: - lawb = waterboard.get_data(wb, data_kind, extent, **kwargs) - if len(lawb) == 0: - logger.info(f"No {data_kind} for {wb} found within model area") - continue - if drop_duplicates: - mask = lawb.index.duplicated() - if mask.any(): - msg = "Dropping {} level area(s) of {} with duplicate indexes" - logger.warning(msg.format(mask.sum(), wb)) - lawb = lawb.loc[~mask] - - la[wb] = lawb - mask = ~la[wb].is_valid + for wb in wbs: + logger.info(f"Downloading {data_kind} for {wb}") + try: + lawb = waterboard.get_data(wb, data_kind, extent, **kwargs) + if len(lawb) == 0: + logger.info(f"No {data_kind} for {wb} found within model area") + continue + if drop_duplicates: + mask = lawb.index.duplicated() if mask.any(): - logger.warning( - f"{mask.sum()} geometries of level areas of {wb} are invalid. Thet are made valid by adding a buffer of 0.0." - ) - # first copy to prevent ValueError: assignment destination is read-only - la[wb] = la[wb].copy() - la[wb].loc[mask, "geometry"] = la[wb][mask].buffer(0.0) - except Exception as e: - if str(e) == f"{data_kind} not available for {wb}": - logger.warning(e) - elif raise_exceptions: - raise - else: - logger.warning(e) + msg = "Dropping {} level area(s) of {} with duplicate indexes" + logger.warning(msg.format(mask.sum(), wb)) + lawb = lawb.loc[~mask] + + la[wb] = lawb + mask = ~la[wb].is_valid + if mask.any(): + logger.warning( + f"{mask.sum()} geometries of level areas of {wb} are invalid. Thet are made valid by adding a buffer of 0.0." + ) + # first copy to prevent ValueError: assignment destination is read-only + la[wb] = la[wb].copy() + la[wb].loc[mask, "geometry"] = la[wb][mask].buffer(0.0) + except Exception as e: + if str(e) == f"{data_kind} not available for {wb}": + logger.warning(e) + elif raise_exceptions: + raise + else: + logger.warning(e) return la def download_watercourses( - gdf, extent=None, config=None, raise_exceptions=True, **kwargs + gdf=None, extent=None, config=None, raise_exceptions=True, **kwargs ): """Download watercourses of bronhouders. Parameters ---------- - gdf : geopandas.GeoDataFrame + gdf : geopandas.GeoDataFrame, optional A GeoDataFrame with surface water features, containing the column "bronhouder". - extent : list, tuple or np.array + Determine the required waterboards for this gdf, when not None. The default is + None. + extent : list, tuple or np.array, optional Model extent (xmin, xmax, ymin, ymax). When extent is None, all data of the - water boards in gdf are downloaded downloaded. + water boards in gdf are downloaded downloaded. The default is None. config : dict, optional A dictionary with information about the webservices of the water boards. When config is None, it is created with nlmod.read.waterboard.get_configuration(). @@ -668,28 +675,46 @@ def download_watercourses( """ if config is None: config = waterboard.get_configuration() - bronhouders = gdf["bronhouder"].unique() + wbs = _get_waterboard_selection(gdf=gdf, extent=extent, config=config) wc = {} data_kind = "watercourses" - for wb in config.keys(): - if config[wb]["bgt_code"] in bronhouders: - logger.info(f"Downloading {data_kind} for {wb}") - try: - wcwb = waterboard.get_data(wb, data_kind, extent, **kwargs) - if len(wcwb) == 0: - logger.info(f"No {data_kind} for {wb} found within model area") - continue - wc[wb] = wcwb - except Exception as e: - if str(e) == f"{data_kind} not available for {wb}": - logger.warning(e) - elif raise_exceptions: - raise - else: - logger.warning(e) + for wb in wbs: + logger.info(f"Downloading {data_kind} for {wb}") + try: + wcwb = waterboard.get_data(wb, data_kind, extent, **kwargs) + if len(wcwb) == 0: + logger.info(f"No {data_kind} for {wb} found within model area") + continue + wc[wb] = wcwb + except Exception as e: + if str(e) == f"{data_kind} not available for {wb}": + logger.warning(e) + elif raise_exceptions: + raise + else: + logger.warning(e) return wc +def _get_waterboard_selection(gdf=None, extent=None, config=None): + """Internal method to select waterboards to get data from""" + if config is None: + config = waterboard.get_configuration() + if gdf is None and extent is None: + raise (ValueError("Please specify either gdf or extent")) + + if gdf is not None: + bronhouders = gdf["bronhouder"].unique() + wbs = [] + for wb in config.keys(): + if config[wb]["bgt_code"] in bronhouders: + wbs.append(wb) + elif extent is not None: + wb_gdf = waterboard.get_polygons() + wbs = wb_gdf.index[wb_gdf.intersects(extent_to_polygon(extent))] + return wbs + + def add_stages_from_waterboards( gdf, la=None, extent=None, columns=None, config=None, min_total_overlap=0.0 ): From cde1359637b4964ac9b12cec5648bc7e0349a5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Fri, 5 Apr 2024 16:21:38 +0200 Subject: [PATCH 22/85] Solve bug where there can be multiple tiles in ahn reader with the same name --- nlmod/read/ahn.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nlmod/read/ahn.py b/nlmod/read/ahn.py index cf88e869..ce5484bb 100644 --- a/nlmod/read/ahn.py +++ b/nlmod/read/ahn.py @@ -3,6 +3,7 @@ import matplotlib.pyplot as plt import numpy as np +import pandas as pd import geopandas as gpd import rasterio import rioxarray @@ -412,6 +413,11 @@ def _download_and_combine_tiles(tiles, identifier, extent, as_data_array): datasets = [] for name in tqdm(tiles.index, desc=f"Downloading tiles of {identifier}"): url = tiles.at[name, identifier] + if isinstance(url, pd.Series): + logger.warning( + f"Multiple tiles with the same name: {name}. Choosing the first one." + ) + url = url.iloc[0] path = url.split("/")[-1].replace(".zip", ".TIF") if path.lower().endswith(".tif.tif"): path = path[:-4] From 68ee919b12dd7fa49abbed18b6cf82209b98df87 Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Mon, 15 Apr 2024 10:06:36 +0200 Subject: [PATCH 23/85] Update cache.py (#326) * Update cache.py The netcdf cache function validates the cache by comparing the ds argument and other function arguments to the pickled arguments. If they match, the cache can be used. Currently, just the coordinates of the argument ds and the output ds had to match, introducing two errors: - If the data_vars differ and are used the cache is falsely valid - The coordintates of the ds argument has to match the coordinates of the output ds. This limits the use of the cache function. The PR compares the hash of the coords and data_vars of the ds argument to those that were stored in the pickle together with the cached output ds. Ideally, the cache.cache_netcdf() accepts arguments that specify specifically which data_vars and coords need to be included in the validation check. Beyond the scope of this pr. - Included tests --- nlmod/cache.py | 368 ++++++++++++++++++++++++-------------- nlmod/dims/grid.py | 2 +- nlmod/read/ahn.py | 12 +- nlmod/read/geotop.py | 4 +- nlmod/read/jarkus.py | 4 +- nlmod/read/knmi.py | 2 +- nlmod/read/regis.py | 4 +- nlmod/read/rws.py | 4 +- tests/test_006_caching.py | 164 ++++++++--------- 9 files changed, 325 insertions(+), 239 deletions(-) diff --git a/nlmod/cache.py b/nlmod/cache.py index 93575d78..ad193d06 100644 --- a/nlmod/cache.py +++ b/nlmod/cache.py @@ -53,7 +53,7 @@ def clear_cache(cachedir): logger.info(f"removed {fname_nc}") -def cache_netcdf(func): +def cache_netcdf(coords_2d=False, coords_3d=False, coords_time=False, datavars=None, coords=None, attrs=None): """decorator to read/write the result of a function from/to a file to speed up function calls with the same arguments. Should only be applied to functions that: @@ -81,125 +81,159 @@ def cache_netcdf(func): to the decorated function. This assumes that the decorated function has a docstring with a "Returns" heading. If this is not the case an error is raised when trying to decorate the function. - """ - # add cachedir and cachename to docstring - _update_docstring_and_signature(func) + If all kwargs are left to their defaults, the function caches the full dataset. - @functools.wraps(func) - def decorator(*args, cachedir=None, cachename=None, **kwargs): - # 1 check if cachedir and name are provided - if cachedir is None or cachename is None: - return func(*args, **kwargs) - - if not cachename.endswith(".nc"): - cachename += ".nc" - - fname_cache = os.path.join(cachedir, cachename) # netcdf file - fname_pickle_cache = fname_cache.replace(".nc", ".pklz") - - # create dictionary with function arguments - func_args_dic = {f"arg{i}": args[i] for i in range(len(args))} - func_args_dic.update(kwargs) + Parameters + ---------- + ds : xr.Dataset + Dataset with dimensions and coordinates. + coords_2d : bool, optional + Shorthand for adding 2D coordinates. The default is False. + coords_3d : bool, optional + Shorthand for adding 3D coordinates. The default is False. + coords_time : bool, optional + Shorthand for adding time coordinates. The default is False. + datavars : list, optional + List of data variables to check for. The default is an empty list. + coords : list, optional + List of coordinates to check for. The default is an empty list. + attrs : list, optional + List of attributes to check for. The default is an empty list. + """ - # remove xarray dataset from function arguments - dataset = None - for key in list(func_args_dic.keys()): - if isinstance(func_args_dic[key], xr.Dataset): - if dataset is not None: - raise TypeError( - "function was called with multiple xarray dataset arguments" + def decorator(func): + # add cachedir and cachename to docstring + _update_docstring_and_signature(func) + + @functools.wraps(func) + def wrapper(*args, cachedir=None, cachename=None, **kwargs): + # 1 check if cachedir and name are provided + if cachedir is None or cachename is None: + return func(*args, **kwargs) + + if not cachename.endswith(".nc"): + cachename += ".nc" + + fname_cache = os.path.join(cachedir, cachename) # netcdf file + fname_pickle_cache = fname_cache.replace(".nc", ".pklz") + + # create dictionary with function arguments + func_args_dic = {f"arg{i}": args[i] for i in range(len(args))} + func_args_dic.update(kwargs) + + # remove xarray dataset from function arguments + dataset = None + for key in list(func_args_dic.keys()): + if isinstance(func_args_dic[key], xr.Dataset): + if dataset is not None: + raise TypeError( + "Function was called with multiple xarray dataset arguments. Currently unsupported." + ) + dataset_received = func_args_dic.pop(key) + dataset = ds_contains( + dataset_received, + coords_2d=coords_2d, + coords_3d=coords_3d, + coords_time=coords_time, + datavars=datavars, + coords=coords, + attrs=attrs) + + # only use cache if the cache file and the pickled function arguments exist + if os.path.exists(fname_cache) and os.path.exists(fname_pickle_cache): + # check if you can read the pickle, there are several reasons why a + # pickle can not be read. + try: + with open(fname_pickle_cache, "rb") as f: + func_args_dic_cache = pickle.load(f) + pickle_check = True + except (pickle.UnpicklingError, ModuleNotFoundError): + logger.info("could not read pickle, not using cache") + pickle_check = False + argument_check = False + + # check if the module where the function is defined was changed + # after the cache was created + time_mod_func = _get_modification_time(func) + time_mod_cache = os.path.getmtime(fname_cache) + modification_check = time_mod_cache > time_mod_func + + if not modification_check: + logger.info( + f"module of function {func.__name__} recently modified, not using cache" ) - dataset = func_args_dic.pop(key) - # only use cache if the cache file and the pickled function arguments exist - if os.path.exists(fname_cache) and os.path.exists(fname_pickle_cache): - # check if you can read the pickle, there are several reasons why a - # pickle can not be read. - try: - with open(fname_pickle_cache, "rb") as f: - func_args_dic_cache = pickle.load(f) - pickle_check = True - except (pickle.UnpicklingError, ModuleNotFoundError): - logger.info("could not read pickle, not using cache") - pickle_check = False - argument_check = False + with xr.open_dataset(fname_cache) as cached_ds: + cached_ds.load() - # check if the module where the function is defined was changed - # after the cache was created - time_mod_func = _get_modification_time(func) - time_mod_cache = os.path.getmtime(fname_cache) - modification_check = time_mod_cache > time_mod_func + if pickle_check: + # Ensure that the pickle pairs with the netcdf, see #66. + func_args_dic["_nc_hash"] = dask.base.tokenize(cached_ds) - if not modification_check: - logger.info( - f"module of function {func.__name__} recently modified, not using cache" - ) + if dataset is not None: + # Check the coords of the dataset argument + func_args_dic["_dataset_coords_hash"] = dask.base.tokenize(dict(dataset.coords)) - cached_ds = xr.open_dataset(fname_cache) + # Check the data_vars of the dataset argument + func_args_dic["_dataset_data_vars_hash"] = dask.base.tokenize(dict(dataset.data_vars)) - if pickle_check: - # add netcdf hash to function arguments dic, see #66 - func_args_dic["_nc_hash"] = dask.base.tokenize(cached_ds) - - # check if cache was created with same function arguments as - # function call - argument_check = _same_function_arguments( - func_args_dic, func_args_dic_cache - ) - - cached_ds = _check_for_data_array(cached_ds) - if modification_check and argument_check and pickle_check: - if dataset is None: - logger.info(f"using cached data -> {cachename}") - return cached_ds + # check if cache was created with same function arguments as + # function call + argument_check = _same_function_arguments( + func_args_dic, func_args_dic_cache + ) - # check if cached dataset has same dimension and coordinates - # as current dataset - if _check_ds(dataset, cached_ds): + cached_ds = _check_for_data_array(cached_ds) + if modification_check and argument_check and pickle_check: logger.info(f"using cached data -> {cachename}") return cached_ds - # create cache - result = func(*args, **kwargs) - logger.info(f"caching data -> {cachename}") + # create cache + result = func(*args, **kwargs) + logger.info(f"caching data -> {cachename}") + + if isinstance(result, xr.DataArray): + # set the DataArray as a variable in a new Dataset + result = xr.Dataset({"__xarray_dataarray_variable__": result}) + + if isinstance(result, xr.Dataset): + # close cached netcdf (otherwise it is impossible to overwrite) + if os.path.exists(fname_cache): + with xr.open_dataset(fname_cache) as cached_ds: + cached_ds.load() + + # write netcdf cache + # check if dataset is chunked for writing with dask.delayed + first_data_var = list(result.data_vars.keys())[0] + if result[first_data_var].chunks: + delayed = result.to_netcdf(fname_cache, compute=False) + with ProgressBar(): + delayed.compute() + # close and reopen dataset to ensure data is read from + # disk, and not from opendap + result.close() + result = xr.open_dataset(fname_cache, chunks="auto") + else: + result.to_netcdf(fname_cache) - if isinstance(result, xr.DataArray): - # set the DataArray as a variable in a new Dataset - result = xr.Dataset({"__xarray_dataarray_variable__": result}) + # add netcdf hash to function arguments dic, see #66 + with xr.open_dataset(fname_cache) as temp: + func_args_dic["_nc_hash"] = dask.base.tokenize(temp) - if isinstance(result, xr.Dataset): - # close cached netcdf (otherwise it is impossible to overwrite) - if os.path.exists(fname_cache): - cached_ds = xr.open_dataset(fname_cache) - cached_ds.close() + # Add dataset argument hash to pickle + if dataset is not None: + func_args_dic["_dataset_coords_hash"] = dask.base.tokenize(dict(dataset.coords)) + func_args_dic["_dataset_data_vars_hash"] = dask.base.tokenize(dict(dataset.data_vars)) - # write netcdf cache - # check if dataset is chunked for writing with dask.delayed - first_data_var = list(result.data_vars.keys())[0] - if result[first_data_var].chunks: - delayed = result.to_netcdf(fname_cache, compute=False) - with ProgressBar(): - delayed.compute() - # close and reopen dataset to ensure data is read from - # disk, and not from opendap - result.close() - result = xr.open_dataset(fname_cache, chunks="auto") + # pickle function arguments + with open(fname_pickle_cache, "wb") as fpklz: + pickle.dump(func_args_dic, fpklz) else: - result.to_netcdf(fname_cache) - - # add netcdf hash to function arguments dic, see #66 - temp = xr.open_dataset(fname_cache) - func_args_dic["_nc_hash"] = dask.base.tokenize(temp) - temp.close() - - # pickle function arguments - with open(fname_pickle_cache, "wb") as fpklz: - pickle.dump(func_args_dic, fpklz) - else: - raise TypeError(f"expected xarray Dataset, got {type(result)} instead") - result = _check_for_data_array(result) - return result + raise TypeError(f"expected xarray Dataset, got {type(result)} instead") + result = _check_for_data_array(result) + return result + return wrapper return decorator @@ -318,39 +352,6 @@ def decorator(*args, cachedir=None, cachename=None, **kwargs): return decorator -def _check_ds(ds, ds2): - """Check if two datasets have the same dimensions and coordinates. - - Parameters - ---------- - ds : xr.Dataset - dataset with dimensions and coordinates - ds2 : xr.Dataset - dataset with dimensions and coordinates. This is typically - a cached dataset. - - Returns - ------- - bool - True if the two datasets have the same grid and time discretization. - """ - - for coord in ds2.coords: - if coord in ds.coords: - try: - xr.testing.assert_identical(ds[coord], ds2[coord]) - except AssertionError: - logger.info( - f"coordinate {coord} has different values in cached dataset, not using cache" - ) - return False - else: - logger.info(f"dimension {coord} only present in cache, not using cache") - return False - - return True - - def _same_function_arguments(func_args_dic, func_args_dic_cache): """checks if two dictionaries with function arguments are identical by checking: @@ -577,3 +578,98 @@ def _check_for_data_array(ds): if spatial_ref is not None: ds = ds.assign_coords({"spatial_ref": spatial_ref}) return ds + + +def ds_contains(ds, coords_2d=False, coords_3d=False, coords_time=False, datavars=None, coords=None, attrs=None): + """ + Returns a Dataset containing only the required data. + + If all kwargs are left to their defaults, the function returns the full dataset. + + Parameters + ---------- + ds : xr.Dataset + Dataset with dimensions and coordinates. + coords_2d : bool, optional + Shorthand for adding 2D coordinates. The default is False. + coords_3d : bool, optional + Shorthand for adding 3D coordinates. The default is False. + coords_time : bool, optional + Shorthand for adding time coordinates. The default is False. + datavars : list, optional + List of data variables to check for. The default is an empty list. + coords : list, optional + List of coordinates to check for. The default is an empty list. + attrs : list, optional + List of attributes to check for. The default is an empty list. + + Returns + ------- + ds : xr.Dataset + A Dataset containing only the required data. + + """ + # Return the full dataset if not configured + if ds is None: + raise ValueError("No dataset provided") + elif not coords_2d and not coords_3d and not datavars and not coords and not attrs: + return ds + else: + # Initialize lists + if datavars is None: + datavars = [] + if coords is None: + coords = [] + if attrs is None: + attrs = [] + + # Add coords, datavars and attrs via shorthands + if coords_2d or coords_3d: + coords.append("x") + coords.append("y") + attrs.append("extent") + + if "gridtype" in ds.attrs: + attrs.append("gridtype") + + if "angrot" in ds.attrs: + attrs.append("angrot") + + if coords_3d: + coords.append("layer") + datavars.append("top") + datavars.append("botm") + + if coords_time: + coords.append("time") + datavars.append("steady") + datavars.append("nstp") + datavars.append("tsmult") + attrs.append("start") + attrs.append("time_units") + + # User-friendly error messages + if "northsea" in datavars and "northsea" not in ds.datavars: + raise ValueError("Northsea not in dataset. Run nlmod.read.rws.add_northsea() first.") + + if "time" in coords and "time" not in ds.coords: + raise ValueError("time not in dataset. Run nlmod.time.set_ds_time() first.") + + # User-unfriendly error messages + for datavar in datavars: + if datavar not in ds.datavars: + raise ValueError(f"{datavar} not in dataset.datavars") + + for coord in coords: + if coord not in ds.coords: + raise ValueError(f"{coord} not in dataset.coords") + + for attr in attrs: + if attr not in ds.attrs: + raise ValueError(f"{attr} not in dataset.attrs") + + # Return only the required data + return xr.Dataset( + data_vars={k: ds.data_vars[k] for k in datavars}, + coords={k: ds.coords[k] for k in coords}, + attrs={k: ds.attrs[k] for k in attrs}) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 8893ac05..a5ecdaf1 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -1852,7 +1852,7 @@ def get_vertices(ds, vert_per_cid=4, epsilon=0, rotated=False): return vertices_da -@cache.cache_netcdf +@cache.cache_netcdf(coords_2d=True) def mask_model_edge(ds, idomain=None): """get data array which is 1 for every active cell (defined by idomain) at the boundaries of the model (xmin, xmax, ymin, ymax). Other cells are 0. diff --git a/nlmod/read/ahn.py b/nlmod/read/ahn.py index ce5484bb..1bd2fda8 100644 --- a/nlmod/read/ahn.py +++ b/nlmod/read/ahn.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) -@cache.cache_netcdf +@cache.cache_netcdf(coords_2d=True) def get_ahn(ds=None, identifier="AHN4_DTM_5m", method="average", extent=None): """Get a model dataset with ahn variable. @@ -193,7 +193,7 @@ def get_ahn_along_line(line, ahn=None, dx=None, num=None, method="linear", plot= return z -@cache.cache_netcdf +@cache.cache_netcdf() def get_latest_ahn_from_wcs( extent=None, identifier="dsm_05m", @@ -309,7 +309,7 @@ def get_ahn4_tiles(extent=None): return gdf -@cache.cache_netcdf +@cache.cache_netcdf() def get_ahn1(extent, identifier="ahn1_5m", as_data_array=True): """Download AHN1. @@ -336,7 +336,7 @@ def get_ahn1(extent, identifier="ahn1_5m", as_data_array=True): return da -@cache.cache_netcdf +@cache.cache_netcdf() def get_ahn2(extent, identifier="ahn2_5m", as_data_array=True): """Download AHN2. @@ -360,7 +360,7 @@ def get_ahn2(extent, identifier="ahn2_5m", as_data_array=True): return _download_and_combine_tiles(tiles, identifier, extent, as_data_array) -@cache.cache_netcdf +@cache.cache_netcdf() def get_ahn3(extent, identifier="AHN3_5m_DTM", as_data_array=True): """Download AHN3. @@ -383,7 +383,7 @@ def get_ahn3(extent, identifier="AHN3_5m_DTM", as_data_array=True): return _download_and_combine_tiles(tiles, identifier, extent, as_data_array) -@cache.cache_netcdf +@cache.cache_netcdf() def get_ahn4(extent, identifier="AHN4_DTM_5m", as_data_array=True): """Download AHN4. diff --git a/nlmod/read/geotop.py b/nlmod/read/geotop.py index 5ded82f0..b610f005 100644 --- a/nlmod/read/geotop.py +++ b/nlmod/read/geotop.py @@ -59,7 +59,7 @@ def get_kh_kv_table(kind="Brabant"): return df -@cache.cache_netcdf +@cache.cache_netcdf() def to_model_layers( geotop_ds, strat_props=None, @@ -233,7 +233,7 @@ def to_model_layers( return ds -@cache.cache_netcdf +@cache.cache_netcdf() def get_geotop(extent, url=GEOTOP_URL, probabilities=False): """Get a slice of the geotop netcdf url within the extent, set the x and y coordinates to match the cell centers and keep only the strat and lithok diff --git a/nlmod/read/jarkus.py b/nlmod/read/jarkus.py index e0672a5f..6654352d 100644 --- a/nlmod/read/jarkus.py +++ b/nlmod/read/jarkus.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) -@cache.cache_netcdf +@cache.cache_netcdf() def get_bathymetry(ds, northsea, kind="jarkus", method="average"): """get bathymetry of the Northsea from the jarkus dataset. @@ -92,7 +92,7 @@ def get_bathymetry(ds, northsea, kind="jarkus", method="average"): return ds_out -@cache.cache_netcdf +@cache.cache_netcdf() def get_dataset_jarkus(extent, kind="jarkus", return_tiles=False, time=-1): """Get bathymetry from Jarkus within a certain extent. If return_tiles is False, the following actions are performed: diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index 512f34a1..f6e0b27e 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -@cache.cache_netcdf +@cache.cache_netcdf(coords_2d=True, coords_time=True) def get_recharge(ds, method="linear", most_common_station=False): """add multiple recharge packages to the groundwater flow model with knmi data by following these steps: diff --git a/nlmod/read/regis.py b/nlmod/read/regis.py index 729d7b44..90787dcc 100644 --- a/nlmod/read/regis.py +++ b/nlmod/read/regis.py @@ -16,7 +16,7 @@ # REGIS_URL = 'https://www.dinodata.nl/opendap/hyrax/REGIS/REGIS.nc' -@cache.cache_netcdf +@cache.cache_netcdf() def get_combined_layer_models( extent, regis_botm_layer="AKc", @@ -93,7 +93,7 @@ def get_combined_layer_models( return combined_ds -@cache.cache_netcdf +@cache.cache_netcdf() def get_regis( extent, botm_layer="AKc", diff --git a/nlmod/read/rws.py b/nlmod/read/rws.py index 7af2a991..01d5b4e2 100644 --- a/nlmod/read/rws.py +++ b/nlmod/read/rws.py @@ -37,7 +37,7 @@ def get_gdf_surface_water(ds): return gdf_swater -@cache.cache_netcdf +@cache.cache_netcdf(coords_2d=True) def get_surface_water(ds, da_basename): """create 3 data-arrays from the shapefile with surface water: @@ -91,7 +91,7 @@ def get_surface_water(ds, da_basename): return ds_out -@cache.cache_netcdf +@cache.cache_netcdf(coords_2d=True) def get_northsea(ds, da_name="northsea"): """Get Dataset which is 1 at the northsea and 0 everywhere else. Sea is defined by rws surface water shapefile. diff --git a/tests/test_006_caching.py b/tests/test_006_caching.py index 741c1ffd..5bdfb3e0 100644 --- a/tests/test_006_caching.py +++ b/tests/test_006_caching.py @@ -1,96 +1,86 @@ +import os import tempfile -import pytest -import test_001_model - import nlmod -tmpdir = tempfile.gettempdir() - - -def test_ds_check_true(): - # two models with the same grid and time dicretisation - ds = test_001_model.get_ds_from_cache("small_model") - ds2 = ds.copy() - - check = nlmod.cache._check_ds(ds, ds2) - - assert check - - -def test_ds_check_time_false(): - # two models with a different time discretisation - ds = test_001_model.get_ds_from_cache("small_model") - ds2 = test_001_model.get_ds_time_steady(tmpdir) - - check = nlmod.cache._check_ds(ds, ds2) - - assert not check - - -def test_ds_check_time_attributes_false(): - # two models with a different time discretisation - ds = test_001_model.get_ds_from_cache("small_model") - ds2 = ds.copy() - - ds2.time.attrs["time_units"] = "MONTHS" - - check = nlmod.cache._check_ds(ds, ds2) - - assert not check - -def test_cache_data_array(): +def test_cache_ahn_data_array(): + """Test caching of AHN data array. Does not have dataset as argument.""" extent = [119_900, 120_000, 441_900, 442_000] - ahn_no_cache = nlmod.read.ahn.get_ahn4(extent) - ahn_cached = nlmod.read.ahn.get_ahn4(extent, cachedir=tmpdir, cachename="ahn4.nc") - ahn_cache = nlmod.read.ahn.get_ahn4(extent, cachedir=tmpdir, cachename="ahn4.nc") - assert ahn_cached.equals(ahn_no_cache) - assert ahn_cache.equals(ahn_no_cache) - - -@pytest.mark.slow -def test_ds_check_grid_false(tmpdir): - # two models with a different grid and same time dicretisation - ds = test_001_model.get_ds_from_cache("small_model") - ds2 = test_001_model.get_ds_time_transient(tmpdir) - extent = [99100.0, 99400.0, 489100.0, 489400.0] - regis_ds = nlmod.read.regis.get_combined_layer_models( - extent, - use_regis=True, - use_geotop=False, - cachedir=tmpdir, - cachename="comb.nc", + cache_name = "ahn4.nc" + + with tempfile.TemporaryDirectory() as tmpdir: + assert not os.path.exists(os.path.join(tmpdir, cache_name)), "Cache should not exist yet1" + ahn_no_cache = nlmod.read.ahn.get_ahn4(extent) + assert not os.path.exists(os.path.join(tmpdir, cache_name)), "Cache should not exist yet2" + + ahn_cached = nlmod.read.ahn.get_ahn4(extent, cachedir=tmpdir, cachename=cache_name) + assert os.path.exists(os.path.join(tmpdir, cache_name)), "Cache should have existed by now" + assert ahn_cached.equals(ahn_no_cache) + modification_time1 = os.path.getmtime(os.path.join(tmpdir, cache_name)) + + # Check if the cache is used. If not, cache is rewritten and modification time changes + ahn_cache = nlmod.read.ahn.get_ahn4(extent, cachedir=tmpdir, cachename=cache_name) + assert ahn_cache.equals(ahn_no_cache) + modification_time2 = os.path.getmtime(os.path.join(tmpdir, cache_name)) + assert modification_time1 == modification_time2, "Cache should not be rewritten" + + # Different extent should not lead to using the cache + extent = [119_800, 120_000, 441_900, 442_000] + ahn_cache = nlmod.read.ahn.get_ahn4(extent, cachedir=tmpdir, cachename=cache_name) + modification_time3 = os.path.getmtime(os.path.join(tmpdir, cache_name)) + assert modification_time1 != modification_time3, "Cache should have been rewritten" + + +def test_cache_northsea_data_array(): + """Test caching of AHN data array. Does have dataset as argument.""" + from nlmod.read.rws import get_northsea + ds1 = nlmod.get_ds( + [119_700, 120_000, 441_900, 442_000], + delr=100., + delc=100., + top=0., + botm=[-1., -2.], + kh=10., + kv=1., ) - ds2 = nlmod.base.to_model_ds(regis_ds, delr=50.0, delc=50.0) - - check = nlmod.cache._check_ds(ds, ds2) - - assert not check - - -@pytest.mark.skip("too slow") -def test_use_cached_regis(tmpdir): - extent = [98700.0, 99000.0, 489500.0, 489700.0] - regis_ds1 = nlmod.read.regis.get_regis(extent, cachedir=tmpdir, cachename="reg.nc") - - regis_ds2 = nlmod.read.regis.get_regis(extent, cachedir=tmpdir, cachename="reg.nc") - - assert regis_ds1.equals(regis_ds2) - - -@pytest.mark.skip("too slow") -def test_do_not_use_cached_regis(tmpdir): - # cache regis - extent = [98700.0, 99000.0, 489500.0, 489700.0] - regis_ds1 = nlmod.read.regis.get_regis( - extent, cachedir=tmpdir, cachename="regis.nc" - ) - - # do not use cache because extent is different - extent = [99100.0, 99400.0, 489100.0, 489400.0] - regis_ds2 = nlmod.read.regis.get_regis( - extent, cachedir=tmpdir, cachename="regis.nc" + ds2 = nlmod.get_ds( + [119_800, 120_000, 441_900, 444_000], + delr=100., + delc=100., + top=0., + botm=[-1., -3.], + kh=10., + kv=1., ) - assert not regis_ds1.equals(regis_ds2) + cache_name = "northsea.nc" + + with tempfile.TemporaryDirectory() as tmpdir: + assert not os.path.exists(os.path.join(tmpdir, cache_name)), "Cache should not exist yet1" + out1_no_cache = get_northsea(ds1) + assert not os.path.exists(os.path.join(tmpdir, cache_name)), "Cache should not exist yet2" + + out1_cached = get_northsea(ds1, cachedir=tmpdir, cachename=cache_name) + assert os.path.exists(os.path.join(tmpdir, cache_name)), "Cache should exist by now" + assert out1_cached.equals(out1_no_cache) + modification_time1 = os.path.getmtime(os.path.join(tmpdir, cache_name)) + + # Check if the cache is used. If not, cache is rewritten and modification time changes + out1_cache = get_northsea(ds1, cachedir=tmpdir, cachename=cache_name) + assert out1_cache.equals(out1_no_cache) + modification_time2 = os.path.getmtime(os.path.join(tmpdir, cache_name)) + assert modification_time1 == modification_time2, "Cache should not be rewritten" + + # Only properties of `coords_2d` determine if the cache is used. Cache should still be used. + ds1["toppertje"] = ds1.top + 1 + out1_cache = get_northsea(ds1, cachedir=tmpdir, cachename=cache_name) + assert out1_cache.equals(out1_no_cache) + modification_time2 = os.path.getmtime(os.path.join(tmpdir, cache_name)) + assert modification_time1 == modification_time2, "Cache should not be rewritten" + + # Different extent should not lead to using the cache + out2_cache = get_northsea(ds2, cachedir=tmpdir, cachename=cache_name) + modification_time3 = os.path.getmtime(os.path.join(tmpdir, cache_name)) + assert modification_time1 != modification_time3, "Cache should have been rewritten" + assert not out2_cache.equals(out1_no_cache) From 99d53729831b92c21dbcc850eba1e04b3b58e5bd Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Wed, 17 Apr 2024 12:19:26 +0200 Subject: [PATCH 24/85] fix for #334 and #317 --- nlmod/read/waterboard.py | 2 +- nlmod/read/webservices.py | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/nlmod/read/waterboard.py b/nlmod/read/waterboard.py index bbf0779a..64de1fda 100644 --- a/nlmod/read/waterboard.py +++ b/nlmod/read/waterboard.py @@ -201,7 +201,7 @@ def get_configuration(): "bottom_height": "WS_BODEMHOOGTE", }, "level_areas": { - "url": "https://kaarten.hhnk.nl/arcgis/rest/services/NHFLO/Peilgebied_beheerregister/MapServer", + "url": "https://kaarten.hhnk.nl/arcgis/rest/services/ws/ws_peilgebieden_vigerend/MapServer", "summer_stage": [ "ZOMER", "STREEFPEIL_ZOMER", diff --git a/nlmod/read/webservices.py b/nlmod/read/webservices.py index cd47e59d..2e018824 100644 --- a/nlmod/read/webservices.py +++ b/nlmod/read/webservices.py @@ -179,28 +179,6 @@ def arcrest( [feature["attributes"] for feature in data["features"]] ) - # add peilen to gdf - for col, convert_dic in table.items(): - df[col].replace(convert_dic, inplace=True) - df.set_index(col, inplace=True) - - for oid in gdf["OBJECTID"]: - insert_s = df.loc[ - df["PEILGEBIEDVIGERENDID"] == oid, "WATERHOOGTE" - ] - if insert_s.index.duplicated().any(): - # Error in the database. Reported to Doeke HHNK 20230123 - # Continue with unambiguous values - dup = set(insert_s.index[insert_s.index.duplicated()]) - logger.warning( - f"Duplicate {dup} values for PEILGEBIEDVIGERENDID {oid} while prompting {url}" - ) - insert_s = insert_s[~insert_s.index.duplicated(keep=False)] - - gdf.loc[ - gdf["OBJECTID"] == oid, insert_s.index - ] = insert_s.values - return gdf From ad64f3e77fa8e29422b78e5ce6606052e7e28ab3 Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Thu, 18 Apr 2024 10:51:30 +0200 Subject: [PATCH 25/85] fix for #336 --- nlmod/read/knmi.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index f6e0b27e..027a696b 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -277,20 +277,20 @@ def get_knmi_at_locations(ds, start="2010", end=None, most_common_station=False) stns_ev24 = locations["stn_ev24"].unique() # get knmi data stations closest to any grid cell - oc_knmi_prec = hpd.ObsCollection.from_knmi( - stns=stns_rd, - starts=[start], - ends=[end], - meteo_vars=["RD"], - fill_missing_obs=True, - ) - - oc_knmi_evap = hpd.ObsCollection.from_knmi( - stns=stns_ev24, - starts=[start], - ends=[end], - meteo_vars=["EV24"], - fill_missing_obs=True, - ) + olist_rd = [] + for stnrd in stns_rd: + o = hpd.PrecipitationObs.from_knmi(meteo_var="RD", stn=stnrd) + if o.station != stnrd: + locations["stn_rd"] = locations["stn_rd"].replace(stnrd, o.station) + olist_rd.append(o) + oc_knmi_prec = hpd.ObsCollection(olist_rd) + + olist_ev24 = [] + for stnev24 in stns_ev24: + o = hpd.EvaporationObs.from_knmi(meteo_var="EV24", stn=stnev24) + if o.station != stnev24: + locations["stn_ev24"] = locations["stn_rd"].replace(stnev24, o.station) + olist_ev24.append(o) + oc_knmi_evap = hpd.ObsCollection(olist_ev24) return locations, oc_knmi_prec, oc_knmi_evap From 5587e4e128015980482bf2b8bdfd5e6653ec4267 Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Thu, 18 Apr 2024 11:23:28 +0200 Subject: [PATCH 26/85] fix for the last fix --- nlmod/read/knmi.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index 027a696b..5cc1a4ef 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -279,7 +279,9 @@ def get_knmi_at_locations(ds, start="2010", end=None, most_common_station=False) # get knmi data stations closest to any grid cell olist_rd = [] for stnrd in stns_rd: - o = hpd.PrecipitationObs.from_knmi(meteo_var="RD", stn=stnrd) + o = hpd.PrecipitationObs.from_knmi( + meteo_var="RD", stn=stnrd, start=start, end=end, fill_missing_obs=True + ) if o.station != stnrd: locations["stn_rd"] = locations["stn_rd"].replace(stnrd, o.station) olist_rd.append(o) @@ -287,7 +289,9 @@ def get_knmi_at_locations(ds, start="2010", end=None, most_common_station=False) olist_ev24 = [] for stnev24 in stns_ev24: - o = hpd.EvaporationObs.from_knmi(meteo_var="EV24", stn=stnev24) + o = hpd.EvaporationObs.from_knmi( + meteo_var="EV24", stn=stnev24, start=start, end=end, fill_missing_obs=True + ) if o.station != stnev24: locations["stn_ev24"] = locations["stn_rd"].replace(stnev24, o.station) olist_ev24.append(o) From c41f7af0dd2d7c0330d48981025c5433833bac3d Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Thu, 18 Apr 2024 11:51:26 +0200 Subject: [PATCH 27/85] Small change to cache --- nlmod/cache.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nlmod/cache.py b/nlmod/cache.py index ad193d06..d20a3aeb 100644 --- a/nlmod/cache.py +++ b/nlmod/cache.py @@ -627,6 +627,7 @@ def ds_contains(ds, coords_2d=False, coords_3d=False, coords_time=False, datavar if coords_2d or coords_3d: coords.append("x") coords.append("y") + datavars.append("area") attrs.append("extent") if "gridtype" in ds.attrs: @@ -649,7 +650,7 @@ def ds_contains(ds, coords_2d=False, coords_3d=False, coords_time=False, datavar attrs.append("time_units") # User-friendly error messages - if "northsea" in datavars and "northsea" not in ds.datavars: + if "northsea" in datavars and "northsea" not in ds.data_vars: raise ValueError("Northsea not in dataset. Run nlmod.read.rws.add_northsea() first.") if "time" in coords and "time" not in ds.coords: @@ -657,8 +658,8 @@ def ds_contains(ds, coords_2d=False, coords_3d=False, coords_time=False, datavar # User-unfriendly error messages for datavar in datavars: - if datavar not in ds.datavars: - raise ValueError(f"{datavar} not in dataset.datavars") + if datavar not in ds.data_vars: + raise ValueError(f"{datavar} not in dataset.data_vars") for coord in coords: if coord not in ds.coords: From b5e23cc475bf60a6c77d6bbaa418dff23c74307d Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Thu, 18 Apr 2024 11:55:45 +0200 Subject: [PATCH 28/85] remove duplicate stations from knmi data --- nlmod/read/knmi.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index 5cc1a4ef..9fcc6527 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -277,24 +277,38 @@ def get_knmi_at_locations(ds, start="2010", end=None, most_common_station=False) stns_ev24 = locations["stn_ev24"].unique() # get knmi data stations closest to any grid cell - olist_rd = [] + olist_rd, new_stns_rd = [], [] for stnrd in stns_rd: o = hpd.PrecipitationObs.from_knmi( meteo_var="RD", stn=stnrd, start=start, end=end, fill_missing_obs=True ) + + # if a station has no data in the given period another station is selected if o.station != stnrd: locations["stn_rd"] = locations["stn_rd"].replace(stnrd, o.station) - olist_rd.append(o) + + # only add the station if it does not exist yet + if o.station not in new_stns_rd: + olist_rd.append(o) + new_stns_rd.append(o.station) + oc_knmi_prec = hpd.ObsCollection(olist_rd) - olist_ev24 = [] + olist_ev24, new_stns_ev24 = [], [] for stnev24 in stns_ev24: o = hpd.EvaporationObs.from_knmi( meteo_var="EV24", stn=stnev24, start=start, end=end, fill_missing_obs=True ) + + # if a station has no data in the given period another station is selected if o.station != stnev24: locations["stn_ev24"] = locations["stn_rd"].replace(stnev24, o.station) - olist_ev24.append(o) + + # only add the station if it does not exist yet + if o.station not in new_stns_ev24: + olist_ev24.append(o) + new_stns_ev24.append(o.station) + oc_knmi_evap = hpd.ObsCollection(olist_ev24) return locations, oc_knmi_prec, oc_knmi_evap From e581a422e797438b12c4beb9fa6ba483c750b3a5 Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Fri, 19 Apr 2024 10:36:22 +0200 Subject: [PATCH 29/85] remove confusing ci step --- .github/workflows/ci.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4a524e7..64fbf056 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,16 +44,8 @@ jobs: run: | python -c "import nlmod; nlmod.util.download_mfbinaries()" - - name: Run notebooks - if: ${{ github.event_name == 'push' }} - env: - NHI_GWO_USERNAME: ${{ secrets.NHI_GWO_USERNAME}} - NHI_GWO_PASSWORD: ${{ secrets.NHI_GWO_PASSWORD}} - run: | - py.test ./tests -m "not notebooks" - - name: Run tests only - if: ${{ github.event_name == 'pull_request' }} + if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push'}} env: NHI_GWO_USERNAME: ${{ secrets.NHI_GWO_USERNAME}} NHI_GWO_PASSWORD: ${{ secrets.NHI_GWO_PASSWORD}} From 993e02f0336ef3ea529211fdbe2b9f697b60392d Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Fri, 19 Apr 2024 10:44:34 +0200 Subject: [PATCH 30/85] add deprecation warning #254 --- nlmod/dims/grid.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index a5ecdaf1..99eb512b 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -237,6 +237,12 @@ def modelgrid_from_ds(ds, rotated=True, nlay=None, top=None, botm=None, **kwargs def modelgrid_to_vertex_ds(mg, ds, nodata=-1): """Add information about the calculation-grid to a model dataset.""" + + logger.warning( + "'modelgrid_to_vertex_ds' is deprecated and will be removed in a" + "future version, please use 'modelgrid_to_ds' instead" + ) + # add modelgrid to ds ds["xv"] = ("iv", mg.verts[:, 0]) ds["yv"] = ("iv", mg.verts[:, 1]) From 367c1a9e4222fd6c0b07cedf76b596a06028579c Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Fri, 19 Apr 2024 10:49:43 +0200 Subject: [PATCH 31/85] typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64fbf056..51e25738 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: python -c "import nlmod; nlmod.util.download_mfbinaries()" - name: Run tests only - if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push'}} + if: ${{ github.event_name == 'pull_request' || github.event_name == 'push'}} env: NHI_GWO_USERNAME: ${{ secrets.NHI_GWO_USERNAME}} NHI_GWO_PASSWORD: ${{ secrets.NHI_GWO_PASSWORD}} From 69ccac72488d057fbe48b484bd519d456006643d Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Fri, 19 Apr 2024 12:32:07 +0200 Subject: [PATCH 32/85] fixes for comments Bas --- .github/workflows/ci.yml | 1 - nlmod/dims/grid.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51e25738..60c27dae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,6 @@ jobs: python -c "import nlmod; nlmod.util.download_mfbinaries()" - name: Run tests only - if: ${{ github.event_name == 'pull_request' || github.event_name == 'push'}} env: NHI_GWO_USERNAME: ${{ secrets.NHI_GWO_USERNAME}} NHI_GWO_PASSWORD: ${{ secrets.NHI_GWO_PASSWORD}} diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 99eb512b..133a7538 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -238,9 +238,10 @@ def modelgrid_from_ds(ds, rotated=True, nlay=None, top=None, botm=None, **kwargs def modelgrid_to_vertex_ds(mg, ds, nodata=-1): """Add information about the calculation-grid to a model dataset.""" - logger.warning( + warnings.warn( "'modelgrid_to_vertex_ds' is deprecated and will be removed in a" - "future version, please use 'modelgrid_to_ds' instead" + "future version, please use 'modelgrid_to_ds' instead", + DeprecationWarning, ) # add modelgrid to ds From f8c229e9eff0868c42e2b876699947a4f7a3bbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Fri, 10 May 2024 10:56:13 +0200 Subject: [PATCH 33/85] Add bottleneck to default packages Otherwise nlmod.read.regis.get_regis() would return ModuleNotFoundError: No module named 'bottleneck' --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6729fa58..0fe5b395 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,8 @@ dependencies = [ "matplotlib", "dask", "colorama", - "joblib" + "joblib", + "bottleneck", ] keywords = ["hydrology", "groundwater", "modeling", "Modflow 6", "flopy"] classifiers = [ @@ -60,7 +61,6 @@ full = [ "nlmod[knmi]", "gdown", "geocube", - "bottleneck", "contextily", "scikit-image", ] From 03800266d3af442104f276d7a95b6f215f794880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Fri, 10 May 2024 11:05:47 +0200 Subject: [PATCH 34/85] Copy all files from nlmod.data.geotop in pyproject.toml And not just csv-files --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0fe5b395..26bbcc2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ include-package-data = true [tool.setuptools.package-data] "nlmod.data" = ["*.gleg"] -"nlmod.data.geotop" = ["*.csv"] +"nlmod.data.geotop" = ["*"] "nlmod.data.shapes" = ["*"] "nlmod.bin" = ["mp7_2_002_provisional"] From ba81ff62e7bcf06b98f007c3422ef27fb804cc4f Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Tue, 14 May 2024 15:49:30 +0200 Subject: [PATCH 35/85] Creating new cache used full ds instead of reduced ds (#340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Creating new cache used full ds instead of reduced ds Required to ensure that the cached function does not use data_vars that not explicitly required * Format cache to please codacy * Better support for vertex grids * remove option to store delr/delc as attrs * remove option to store delr/delc as attrs * update polygons_from_model_ds to to use new delr/delc datavars * add delr and delc to datavars in ds_to_structured_grid * remove delr/delc attr check in structured_da_to_ds * get_recharge uses coords3d to determine first active layer * add delr/delc to datavars for surface water functions * formatting * formatting * remove coords that number from 0 to N (these are not listed in coords) * remove delr and delc from datavars is vertexgrid * do not check time attrs start and time_units * do not check time coord attributes * formatting * skip delr/delc when writing files * rename bathymetry tests * comment still in dutch: bathymetry * comment still in dutch: bathymetry * use last delr to set extent * use brackets (was failing for me without) * Cache: Implemented inline suggestions made by David --------- Co-authored-by: Davíd Brakenhoff --- docs/examples/02_surface_water.ipynb | 10 +- docs/examples/cache_example.py | 2 +- nlmod/cache.py | 225 ++++++++++++++++++--------- nlmod/dims/base.py | 17 +- nlmod/dims/grid.py | 32 ++-- nlmod/dims/resample.py | 14 +- nlmod/gis.py | 7 +- nlmod/read/jarkus.py | 4 +- nlmod/read/knmi.py | 2 +- nlmod/read/rws.py | 6 +- pyproject.toml | 4 + tests/test_004_northsea.py | 4 +- 12 files changed, 219 insertions(+), 108 deletions(-) diff --git a/docs/examples/02_surface_water.ipynb b/docs/examples/02_surface_water.ipynb index f5d8cb8ae..c0a7033b 100644 --- a/docs/examples/02_surface_water.ipynb +++ b/docs/examples/02_surface_water.ipynb @@ -490,7 +490,7 @@ "xlim = ax.get_xlim()\n", "ylim = ax.get_ylim()\n", "gwf.modelgrid.plot(ax=ax)\n", - "ax.set_xlim(xlim[0], xlim[0] + ds.delr * 1.1)\n", + "ax.set_xlim(xlim[0], xlim[0] + ds.delr[-1] * 1.1)\n", "ax.set_ylim(ylim)\n", "ax.set_title(f\"Surface water shapes in cell: {cid}\")" ] @@ -788,6 +788,14 @@ "cbar = fig.colorbar(qm, shrink=1.0)\n", "cbar.set_label(\"head [m NAP]\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1cbc42bf", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/docs/examples/cache_example.py b/docs/examples/cache_example.py index d1ded5d6..5d33d7b0 100644 --- a/docs/examples/cache_example.py +++ b/docs/examples/cache_example.py @@ -3,7 +3,7 @@ import xarray as xr -@nlmod.cache.cache_netcdf +@nlmod.cache.cache_netcdf() def func_to_create_a_dataset(number): """create a dataarray as an example for the caching method. diff --git a/nlmod/cache.py b/nlmod/cache.py index d20a3aeb..6ccd717f 100644 --- a/nlmod/cache.py +++ b/nlmod/cache.py @@ -53,8 +53,15 @@ def clear_cache(cachedir): logger.info(f"removed {fname_nc}") -def cache_netcdf(coords_2d=False, coords_3d=False, coords_time=False, datavars=None, coords=None, attrs=None): - """decorator to read/write the result of a function from/to a file to speed +def cache_netcdf( + coords_2d=False, + coords_3d=False, + coords_time=False, + datavars=None, + coords=None, + attrs=None, +): + """Decorator to read/write the result of a function from/to a file to speed up function calls with the same arguments. Should only be applied to functions that: @@ -118,27 +125,54 @@ def wrapper(*args, cachedir=None, cachename=None, **kwargs): fname_cache = os.path.join(cachedir, cachename) # netcdf file fname_pickle_cache = fname_cache.replace(".nc", ".pklz") - # create dictionary with function arguments - func_args_dic = {f"arg{i}": args[i] for i in range(len(args))} - func_args_dic.update(kwargs) + # adjust args and kwargs with minimal dataset + args_adj = [] + kwargs_adj = {} - # remove xarray dataset from function arguments - dataset = None - for key in list(func_args_dic.keys()): - if isinstance(func_args_dic[key], xr.Dataset): - if dataset is not None: - raise TypeError( - "Function was called with multiple xarray dataset arguments. Currently unsupported." - ) - dataset_received = func_args_dic.pop(key) - dataset = ds_contains( - dataset_received, + datasets = [] + func_args_dic = {} + + for i, arg in enumerate(args): + if isinstance(arg, xr.Dataset): + arg_adj = ds_contains( + arg, coords_2d=coords_2d, coords_3d=coords_3d, coords_time=coords_time, datavars=datavars, coords=coords, - attrs=attrs) + attrs=attrs, + ) + args_adj.append(arg_adj) + datasets.append(arg_adj) + else: + args_adj.append(arg) + func_args_dic[f"arg{i}"] = arg + + for key, arg in kwargs.items(): + if isinstance(arg, xr.Dataset): + arg_adj = ds_contains( + arg, + coords_2d=coords_2d, + coords_3d=coords_3d, + coords_time=coords_time, + datavars=datavars, + coords=coords, + attrs=attrs, + ) + kwargs_adj[key] = arg_adj + datasets.append(arg_adj) + else: + kwargs_adj[key] = arg + func_args_dic[key] = arg + + if len(datasets) == 0: + dataset = None + elif len(datasets) == 1: + dataset = datasets[0] + else: + msg = "Function was called with multiple xarray dataset arguments. Currently unsupported." + raise NotImplementedError(msg) # only use cache if the cache file and the pickled function arguments exist if os.path.exists(fname_cache) and os.path.exists(fname_pickle_cache): @@ -173,10 +207,14 @@ def wrapper(*args, cachedir=None, cachename=None, **kwargs): if dataset is not None: # Check the coords of the dataset argument - func_args_dic["_dataset_coords_hash"] = dask.base.tokenize(dict(dataset.coords)) + func_args_dic["_dataset_coords_hash"] = dask.base.tokenize( + dict(dataset.coords) + ) # Check the data_vars of the dataset argument - func_args_dic["_dataset_data_vars_hash"] = dask.base.tokenize(dict(dataset.data_vars)) + func_args_dic["_dataset_data_vars_hash"] = dask.base.tokenize( + dict(dataset.data_vars) + ) # check if cache was created with same function arguments as # function call @@ -190,7 +228,7 @@ def wrapper(*args, cachedir=None, cachename=None, **kwargs): return cached_ds # create cache - result = func(*args, **kwargs) + result = func(*args_adj, **kwargs_adj) logger.info(f"caching data -> {cachename}") if isinstance(result, xr.DataArray): @@ -205,7 +243,7 @@ def wrapper(*args, cachedir=None, cachename=None, **kwargs): # write netcdf cache # check if dataset is chunked for writing with dask.delayed - first_data_var = list(result.data_vars.keys())[0] + first_data_var = next(iter(result.data_vars.keys())) if result[first_data_var].chunks: delayed = result.to_netcdf(fname_cache, compute=False) with ProgressBar(): @@ -223,23 +261,28 @@ def wrapper(*args, cachedir=None, cachename=None, **kwargs): # Add dataset argument hash to pickle if dataset is not None: - func_args_dic["_dataset_coords_hash"] = dask.base.tokenize(dict(dataset.coords)) - func_args_dic["_dataset_data_vars_hash"] = dask.base.tokenize(dict(dataset.data_vars)) + func_args_dic["_dataset_coords_hash"] = dask.base.tokenize( + dict(dataset.coords) + ) + func_args_dic["_dataset_data_vars_hash"] = dask.base.tokenize( + dict(dataset.data_vars) + ) # pickle function arguments with open(fname_pickle_cache, "wb") as fpklz: pickle.dump(func_args_dic, fpklz) else: - raise TypeError(f"expected xarray Dataset, got {type(result)} instead") - result = _check_for_data_array(result) - return result + msg = f"expected xarray Dataset, got {type(result)} instead" + raise TypeError(msg) + return _check_for_data_array(result) + return wrapper return decorator def cache_pickle(func): - """decorator to read/write the result of a function from/to a file to speed + """Decorator to read/write the result of a function from/to a file to speed up function calls with the same arguments. Should only be applied to functions that: @@ -262,7 +305,6 @@ def cache_pickle(func): docstring with a "Returns" heading. If this is not the case an error is raised when trying to decorate the function. """ - # add cachedir and cachename to docstring _update_docstring_and_signature(func) @@ -346,14 +388,15 @@ def decorator(*args, cachedir=None, cachename=None, **kwargs): with open(fname_pickle_cache, "wb") as fpklz: pickle.dump(func_args_dic, fpklz) else: - raise TypeError(f"expected DataFrame, got {type(result)} instead") + msg = f"expected DataFrame, got {type(result)} instead" + raise TypeError(msg) return result return decorator def _same_function_arguments(func_args_dic, func_args_dic_cache): - """checks if two dictionaries with function arguments are identical by + """Checks if two dictionaries with function arguments are identical by checking: 1. if they have the same keys 2. if the items have the same type @@ -361,7 +404,7 @@ def _same_function_arguments(func_args_dic, func_args_dic_cache): float, bool, str, bytes, list, tuple, dict, np.ndarray, xr.DataArray, - flopy.mf6.ModflowGwf) + flopy.mf6.ModflowGwf). Parameters ---------- @@ -381,7 +424,7 @@ def _same_function_arguments(func_args_dic, func_args_dic_cache): """ for key, item in func_args_dic.items(): # check if cache and function call have same argument names - if key not in func_args_dic_cache.keys(): + if key not in func_args_dic_cache: logger.info( "cache was created using different function arguments, do not use cached data" ) @@ -439,15 +482,23 @@ def _same_function_arguments(func_args_dic, func_args_dic_cache): mfgrid1 = {k: v for k, v in item.mfgrid.__dict__.items() if k not in excl} mfgrid2 = {k: v for k, v in i2.mfgrid.__dict__.items() if k not in excl} - is_same_length_props = all(np.all(np.size(v) == np.size(mfgrid2[k])) for k, v in mfgrid1.items()) + is_same_length_props = all( + np.all(np.size(v) == np.size(mfgrid2[k])) for k, v in mfgrid1.items() + ) - if not is_method_equal or mfgrid1.keys() != mfgrid2.keys() or not is_same_length_props: + if ( + not is_method_equal + or mfgrid1.keys() != mfgrid2.keys() + or not is_same_length_props + ): logger.info( "cache was created using different gridintersect, do not use cached data" ) return False - is_other_props_equal = all(np.all(v == mfgrid2[k]) for k, v in mfgrid1.items()) + is_other_props_equal = all( + np.all(v == mfgrid2[k]) for k, v in mfgrid1.items() + ) if not is_other_props_equal: logger.info( @@ -510,7 +561,8 @@ def _update_docstring_and_signature(func): cur_param = cur_param[:-1] else: add_kwargs = None - new_param = cur_param + ( + new_param = ( + *cur_param, inspect.Parameter( "cachedir", inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None ), @@ -519,7 +571,7 @@ def _update_docstring_and_signature(func): ), ) if add_kwargs is not None: - new_param = new_param + (add_kwargs,) + new_param = (*new_param, add_kwargs) sig = sig.replace(parameters=new_param) func.__signature__ = sig @@ -541,15 +593,14 @@ def _update_docstring_and_signature(func): " filename of netcdf cache. If None no cache is used." " Default is None.\n\n Returns" ) - new_doc = "".join((mod_before, after)) + new_doc = f"{mod_before}{after}" func.__doc__ = new_doc return def _check_for_data_array(ds): - """ - Check if the saved NetCDF-file represents a DataArray or a Dataset, and return this - data-variable. + """Check if the saved NetCDF-file represents a DataArray or a Dataset, and return + this data-variable. The file contains a DataArray when a variable called "__xarray_dataarray_variable__" is present in the Dataset. If so, return a DataArray, otherwise return the Dataset. @@ -566,13 +617,9 @@ def _check_for_data_array(ds): ------- ds : xr.Dataset or xr.DataArray A Dataset or DataArray containing the cached data. - """ if "__xarray_dataarray_variable__" in ds: - if "spatial_ref" in ds: - spatial_ref = ds.spatial_ref - else: - spatial_ref = None + spatial_ref = ds.spatial_ref if "spatial_ref" in ds else None # the method returns a DataArray, so we return only this DataArray ds = ds["__xarray_dataarray_variable__"] if spatial_ref is not None: @@ -580,9 +627,16 @@ def _check_for_data_array(ds): return ds -def ds_contains(ds, coords_2d=False, coords_3d=False, coords_time=False, datavars=None, coords=None, attrs=None): - """ - Returns a Dataset containing only the required data. +def ds_contains( + ds, + coords_2d=False, + coords_3d=False, + coords_time=False, + datavars=None, + coords=None, + attrs=None, +): + """Returns a Dataset containing only the required data. If all kwargs are left to their defaults, the function returns the full dataset. @@ -607,21 +661,23 @@ def ds_contains(ds, coords_2d=False, coords_3d=False, coords_time=False, datavar ------- ds : xr.Dataset A Dataset containing only the required data. - """ # Return the full dataset if not configured if ds is None: - raise ValueError("No dataset provided") - elif not coords_2d and not coords_3d and not datavars and not coords and not attrs: + msg = "No dataset provided" + raise ValueError(msg) + if not coords_2d and not coords_3d and not datavars and not coords and not attrs: return ds - else: - # Initialize lists - if datavars is None: - datavars = [] - if coords is None: - coords = [] - if attrs is None: - attrs = [] + + isvertex = ds.attrs["gridtype"] == "vertex" + + # Initialize lists + if datavars is None: + datavars = [] + if coords is None: + coords = [] + if attrs is None: + attrs = [] # Add coords, datavars and attrs via shorthands if coords_2d or coords_3d: @@ -629,9 +685,18 @@ def ds_contains(ds, coords_2d=False, coords_3d=False, coords_time=False, datavar coords.append("y") datavars.append("area") attrs.append("extent") - - if "gridtype" in ds.attrs: - attrs.append("gridtype") + attrs.append("gridtype") + + if isvertex: + datavars.append("xv") + datavars.append("yv") + datavars.append("icvert") + + # TODO: temporary fix until delr/delc are completely removed + if "delr" in datavars: + datavars.remove("delr") + if "delc" in datavars: + datavars.remove("delc") if "angrot" in ds.attrs: attrs.append("angrot") @@ -646,31 +711,45 @@ def ds_contains(ds, coords_2d=False, coords_3d=False, coords_time=False, datavar datavars.append("steady") datavars.append("nstp") datavars.append("tsmult") - attrs.append("start") - attrs.append("time_units") - # User-friendly error messages + # User-friendly error messages if missing from ds if "northsea" in datavars and "northsea" not in ds.data_vars: - raise ValueError("Northsea not in dataset. Run nlmod.read.rws.add_northsea() first.") + msg = "Northsea not in dataset. Run nlmod.read.rws.add_northsea() first." + raise ValueError(msg) - if "time" in coords and "time" not in ds.coords: - raise ValueError("time not in dataset. Run nlmod.time.set_ds_time() first.") + if coords_time: + if "time" not in ds.coords: + msg = "time not in dataset. Run nlmod.time.set_ds_time() first." + raise ValueError(msg) + + # Check if time-coord is complete + time_attrs_required = ["start", "time_units"] + + for t_attr in time_attrs_required: + if t_attr not in ds["time"].attrs: + msg = f"{t_attr} not in dataset['time'].attrs. " +\ + "Run nlmod.time.set_ds_time() to set time." + raise ValueError(msg) # User-unfriendly error messages for datavar in datavars: if datavar not in ds.data_vars: - raise ValueError(f"{datavar} not in dataset.data_vars") + msg = f"{datavar} not in dataset.data_vars" + raise ValueError(msg) for coord in coords: if coord not in ds.coords: - raise ValueError(f"{coord} not in dataset.coords") + msg = f"{coord} not in dataset.coords" + raise ValueError(msg) for attr in attrs: if attr not in ds.attrs: - raise ValueError(f"{attr} not in dataset.attrs") + msg = f"{attr} not in dataset.attrs" + raise ValueError(msg) # Return only the required data return xr.Dataset( data_vars={k: ds.data_vars[k] for k in datavars}, coords={k: ds.coords[k] for k in coords}, - attrs={k: ds.attrs[k] for k in attrs}) + attrs={k: ds.attrs[k] for k in attrs}, + ) diff --git a/nlmod/dims/base.py b/nlmod/dims/base.py index c652d246..3ec3f946 100644 --- a/nlmod/dims/base.py +++ b/nlmod/dims/base.py @@ -15,7 +15,7 @@ def set_ds_attrs(ds, model_name, model_ws, mfversion="mf6", exe_name=None): - """set the attribute of a model dataset. + """Set the attribute of a model dataset. Parameters ---------- @@ -161,8 +161,9 @@ def to_model_ds( if delc is None: delc = delr if isinstance(delr, (numbers.Number)) and isinstance(delc, (numbers.Number)): - ds["area"] = ("y", "x"), ds.delr * ds.delc * np.ones( - (ds.sizes["y"], ds.sizes["x"]) + ds["area"] = ( + ("y", "x"), + delr * delc * np.ones((ds.sizes["y"], ds.sizes["x"])), ) elif isinstance(delr, np.ndarray) and isinstance(delc, np.ndarray): ds["area"] = ("y", "x"), np.outer(delc, delr) @@ -367,15 +368,9 @@ def _get_structured_grid_ds( ) # set delr and delc delr = np.diff(xedges) - if len(np.unique(delr)) == 1: - ds.attrs["delr"] = np.unique(delr)[0] - else: - ds["delr"] = ("x"), delr + ds["delr"] = ("x"), delr delc = -np.diff(yedges) - if len(np.unique(delc)) == 1: - ds.attrs["delc"] = np.unique(delc)[0] - else: - ds["delc"] = ("y"), delc + ds["delc"] = ("y"), delc if crs is not None: ds.rio.set_crs(crs) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 133a7538..68a4c9b9 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -214,11 +214,11 @@ def modelgrid_from_ds(ds, rotated=True, nlay=None, top=None, botm=None, **kwargs if "delc" in ds: delc = ds["delc"].values else: - delc = np.array([ds.delc] * ds.sizes["y"]) + raise KeyError("delc not in dataset") if "delr" in ds: delr = ds["delr"].values else: - delr = np.array([ds.delr] * ds.sizes["x"]) + raise KeyError("delr not in dataset") modelgrid = StructuredGrid( delc=delc, delr=delr, @@ -1921,7 +1921,7 @@ def mask_model_edge(ds, idomain=None): def polygons_from_model_ds(model_ds): - """create polygons of each cell in a model dataset. + """Create polygons of each cell in a model dataset. Parameters ---------- @@ -1940,19 +1940,33 @@ def polygons_from_model_ds(model_ds): """ if model_ds.gridtype == "structured": - # check if coördinates are consistent with delr/delc values + # TODO: update with new delr/delc calculation + # check if coordinates are consistent with delr/delc values delr_x = np.unique(model_ds.x.values[1:] - model_ds.x.values[:-1]) delc_y = np.unique(model_ds.y.values[:-1] - model_ds.y.values[1:]) - if not ((delr_x == model_ds.delr) and (delc_y == model_ds.delc)): + + delr = np.unique(model_ds.delr) + if len(delr) > 1: + raise ValueError("delr is variable") + else: + delr = delr.item() + + delc = np.unique(model_ds.delc) + if len(delc) > 1: + raise ValueError("delc is variable") + else: + delc = delc.item() + + if not ((delr_x == delr) and (delc_y == delc)): raise ValueError( "delr and delc attributes of model_ds inconsistent " "with x and y coordinates" ) - xmins = model_ds.x - (model_ds.delr * 0.5) - xmaxs = model_ds.x + (model_ds.delr * 0.5) - ymins = model_ds.y - (model_ds.delc * 0.5) - ymaxs = model_ds.y + (model_ds.delc * 0.5) + xmins = model_ds.x - (delr * 0.5) + xmaxs = model_ds.x + (delr * 0.5) + ymins = model_ds.y - (delc * 0.5) + ymaxs = model_ds.y + (delc * 0.5) polygons = [ Polygon( [ diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index c5b536af..73a13427 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -153,20 +153,27 @@ def ds_to_structured_grid( # add new attributes attrs["gridtype"] = "structured" + if isinstance(delr, numbers.Number) and isinstance(delc, numbers.Number): - attrs["delr"] = delr - attrs["delc"] = delc + delr = np.full_like(x, delr) + delc = np.full_like(y, delc) if method in ["nearest", "linear"] and angrot == 0.0: ds_out = ds_in.interp( x=x, y=y, method=method, kwargs={"fill_value": "extrapolate"} ) ds_out.attrs = attrs + # ds_out = ds_out.expand_dims({"ncol": len(x), "nrow": len(y)}) + ds_out["delr"] = ("ncol",), delr + ds_out["delc"] = ("nrow",), delc return ds_out ds_out = xr.Dataset(coords={"y": y, "x": x, "layer": ds_in.layer.data}, attrs=attrs) for var in ds_in.data_vars: ds_out[var] = structured_da_to_ds(ds_in[var], ds_out, method=method) + # ds_out = ds_out.expand_dims({"ncol": len(x), "nrow": len(y)}) + ds_out["delr"] = ("x",), delr + ds_out["delc"] = ("y",), delc return ds_out @@ -519,9 +526,6 @@ def structured_da_to_ds(da, ds, method="average", nodata=np.NaN): # xmin, xmax, ymin, ymax dx = (ds.attrs["extent"][1] - ds.attrs["extent"][0]) / len(ds.x) dy = (ds.attrs["extent"][3] - ds.attrs["extent"][2]) / len(ds.y) - elif "delr" in ds.attrs and "delc" in ds.attrs: - dx = ds.attrs["delr"] - dy = ds.attrs["delc"] else: raise ValueError( "No extent or delr and delc in ds. Cannot determine affine." diff --git a/nlmod/gis.py b/nlmod/gis.py index 17d4709c..3998791f 100644 --- a/nlmod/gis.py +++ b/nlmod/gis.py @@ -143,6 +143,9 @@ def struc_da_to_gdf(model_ds, data_variables, polygons=None, dealing_with_time=" raise ValueError( f"expected dimensions ('layer', 'y', 'x'), got {da.dims}" ) + # TODO: remove when delr/delc are removed as data vars + elif da_name in ["delr", "delc"]: + continue else: raise NotImplementedError( f"expected two or three dimensions got {no_dims} for data variable {da_name}" @@ -286,7 +289,9 @@ def ds_to_vector_file( if model_ds.gridtype == "structured": gdf = struc_da_to_gdf(model_ds, (da_name,), polygons=polygons) elif model_ds.gridtype == "vertex": - gdf = vertex_da_to_gdf(model_ds, (da_name,), polygons=polygons) + # TODO: remove when delr/dec are removed + if da_name not in ["delr", "delc"]: + gdf = vertex_da_to_gdf(model_ds, (da_name,), polygons=polygons) if driver == "GPKG": gdf.to_file(fname_gpkg, layer=da_name, driver=driver) else: diff --git a/nlmod/read/jarkus.py b/nlmod/read/jarkus.py index 6654352d..5d962973 100644 --- a/nlmod/read/jarkus.py +++ b/nlmod/read/jarkus.py @@ -75,7 +75,7 @@ def get_bathymetry(ds, northsea, kind="jarkus", method="average"): # fill nan values in bathymetry da_bathymetry_filled = fillnan_da(da_bathymetry_raw) - # bathymetrie mag nooit groter zijn dan NAP 0.0 + # bathymetry can never be larger than NAP 0.0 da_bathymetry_filled = xr.where(da_bathymetry_filled > 0, 0, da_bathymetry_filled) # bathymetry projected on model grid @@ -272,7 +272,7 @@ def add_bathymetry_to_top_bot_kh_kv(ds, bathymetry, fill_mask, kh_sea=10, kv_sea ds["kv"][lay] = xr.where(fill_mask, kv_sea, ds["kv"][lay]) - # reset bot for all layers based on bathymetrie + # reset bot for all layers based on bathymetry for lay in range(1, ds.sizes["layer"]): ds["botm"][lay] = np.where( ds["botm"][lay] > ds["botm"][lay - 1], diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index 9fcc6527..b142848c 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -@cache.cache_netcdf(coords_2d=True, coords_time=True) +@cache.cache_netcdf(coords_3d=True, coords_time=True) def get_recharge(ds, method="linear", most_common_station=False): """add multiple recharge packages to the groundwater flow model with knmi data by following these steps: diff --git a/nlmod/read/rws.py b/nlmod/read/rws.py index 01d5b4e2..4b00e3c3 100644 --- a/nlmod/read/rws.py +++ b/nlmod/read/rws.py @@ -37,7 +37,8 @@ def get_gdf_surface_water(ds): return gdf_swater -@cache.cache_netcdf(coords_2d=True) +# TODO: temporary fix until delr/delc are removed completely +@cache.cache_netcdf(coords_3d=True, datavars=["delr", "delc"]) def get_surface_water(ds, da_basename): """create 3 data-arrays from the shapefile with surface water: @@ -91,7 +92,8 @@ def get_surface_water(ds, da_basename): return ds_out -@cache.cache_netcdf(coords_2d=True) +# TODO: temporary fix until delr/delc are removed completely +@cache.cache_netcdf(coords_2d=True, datavars=["delc", "delr"]) def get_northsea(ds, da_name="northsea"): """Get Dataset which is 1 at the northsea and 0 everywhere else. Sea is defined by rws surface water shapefile. diff --git a/pyproject.toml b/pyproject.toml index 26bbcc2d..67f499d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,6 +102,10 @@ line-length = 88 [tool.isort] profile = "black" +[tool.ruff] +line-length = 88 +extend-include = ["*.ipynb"] + [tool.pytest.ini_options] addopts = "--strict-markers --durations=0 --cov-report xml:coverage.xml --cov nlmod -v" markers = ["notebooks: run notebooks", "slow: slow tests", "skip: skip tests"] diff --git a/tests/test_004_northsea.py b/tests/test_004_northsea.py index 2e49171d..d71ccea4 100644 --- a/tests/test_004_northsea.py +++ b/tests/test_004_northsea.py @@ -60,7 +60,7 @@ def test_get_bathymetry_seamodel(): assert (~ds_bathymetry.bathymetry.isnull()).sum() > 0 -def test_get_bathymetrie_nosea(): +def test_get_bathymetry_nosea(): # model without sea ds = test_001_model.get_ds_from_cache("small_model") ds.update(nlmod.read.rws.get_northsea(ds)) @@ -69,7 +69,7 @@ def test_get_bathymetrie_nosea(): assert (~ds_bathymetry.bathymetry.isnull()).sum() == 0 -def test_add_bathymetrie_to_top_bot_kh_kv_seamodel(): +def test_add_bathymetry_to_top_bot_kh_kv_seamodel(): # model with sea ds = test_001_model.get_ds_from_cache("basic_sea_model") ds.update(nlmod.read.rws.get_northsea(ds)) From 0796613d201745c7ceebd517e0a12d6e3ae327cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 15 May 2024 11:58:52 +0200 Subject: [PATCH 36/85] Add bottleneck also in documentation to required packages --- README.md | 3 ++- docs/getting_started.rst | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 483db2a5..1bfec90f 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,10 @@ Install the module with pip: * `dask` * `colorama` * `joblib` +* `bottleneck` There are some optional dependecies, only needed (and imported) in a single method. -Examples of this are `bottleneck` (used in calculate_gxg), `geocube` (used in +Examples of this are `geocube` (used in add_min_ahn_to_gdf), `h5netcdf` (used for hdf5 files backend in xarray), `scikit-image` (used in calculate_sea_coverage). To install `nlmod` with the optional dependencies use: diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 2e20eff5..4d995c5c 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -118,10 +118,10 @@ potential solutions. - dask - colorama - joblib +- bottleneck On top of that there are some optional dependecies: -- bottleneck (used in calculate_gxg) - geocube (used in add_min_ahn_to_gdf) - h5netcdf (used for the hdf5 backend of xarray) - scikit-image (used in calculate_sea_coverage) From 73a550f37bba7fb1d82f304a655faada734ee43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 15 May 2024 12:09:06 +0200 Subject: [PATCH 37/85] Change version of dev branch --- nlmod/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlmod/version.py b/nlmod/version.py index b6f9c6c1..5e1c0340 100644 --- a/nlmod/version.py +++ b/nlmod/version.py @@ -1,7 +1,7 @@ from importlib import metadata from platform import python_version -__version__ = "0.7.2" +__version__ = "0.7.3b" def show_versions() -> None: From 3c23366df244b773b89662523be4e238c72fcab8 Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Fri, 17 May 2024 14:50:45 +0200 Subject: [PATCH 38/85] Cache: added shorthand for model ds attributes (#345) * Cache: added shorthand for model ds attributes * Update cache.py * Please Codacy * Update cache.py * Treat transport flag as required model ds attribute * Cache: Exclude "created_on" attr --- nlmod/cache.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/nlmod/cache.py b/nlmod/cache.py index 6ccd717f..3d961f37 100644 --- a/nlmod/cache.py +++ b/nlmod/cache.py @@ -57,6 +57,7 @@ def cache_netcdf( coords_2d=False, coords_3d=False, coords_time=False, + attrs_ds=False, datavars=None, coords=None, attrs=None, @@ -101,6 +102,8 @@ def cache_netcdf( Shorthand for adding 3D coordinates. The default is False. coords_time : bool, optional Shorthand for adding time coordinates. The default is False. + attrs_ds : bool, optional + Shorthand for adding model dataset attributes. The default is False. datavars : list, optional List of data variables to check for. The default is an empty list. coords : list, optional @@ -139,6 +142,7 @@ def wrapper(*args, cachedir=None, cachename=None, **kwargs): coords_2d=coords_2d, coords_3d=coords_3d, coords_time=coords_time, + attrs_ds=attrs_ds, datavars=datavars, coords=coords, attrs=attrs, @@ -156,6 +160,7 @@ def wrapper(*args, cachedir=None, cachename=None, **kwargs): coords_2d=coords_2d, coords_3d=coords_3d, coords_time=coords_time, + attrs_ds=attrs_ds, datavars=datavars, coords=coords, attrs=attrs, @@ -632,6 +637,7 @@ def ds_contains( coords_2d=False, coords_3d=False, coords_time=False, + attrs_ds=False, datavars=None, coords=None, attrs=None, @@ -650,6 +656,8 @@ def ds_contains( Shorthand for adding 3D coordinates. The default is False. coords_time : bool, optional Shorthand for adding time coordinates. The default is False. + attrs_ds : bool, optional + Shorthand for adding model dataset attributes. The default is False. datavars : list, optional List of data variables to check for. The default is an empty list. coords : list, optional @@ -666,7 +674,10 @@ def ds_contains( if ds is None: msg = "No dataset provided" raise ValueError(msg) - if not coords_2d and not coords_3d and not datavars and not coords and not attrs: + isdefault_args = not any( + [coords_2d, coords_3d, coords_time, attrs_ds, datavars, coords, attrs] + ) + if isdefault_args: return ds isvertex = ds.attrs["gridtype"] == "vertex" @@ -699,7 +710,9 @@ def ds_contains( datavars.remove("delc") if "angrot" in ds.attrs: - attrs.append("angrot") + # set by `nlmod.base.to_model_ds()` and `nlmod.dims.resample._set_angrot_attributes()` + attrs_angrot_required = ["angrot", "xorigin", "yorigin"] + attrs.extend(attrs_angrot_required) if coords_3d: coords.append("layer") @@ -712,6 +725,11 @@ def ds_contains( datavars.append("nstp") datavars.append("tsmult") + if attrs_ds: + # set by `nlmod.base.to_model_ds()` and `nlmod.base.set_ds_attrs()`, excluding "created_on" + attrs_ds_required = ["model_name", "mfversion", "exe_name", "model_ws", "figdir", "cachedir", "transport"] + attrs.extend(attrs_ds_required) + # User-friendly error messages if missing from ds if "northsea" in datavars and "northsea" not in ds.data_vars: msg = "Northsea not in dataset. Run nlmod.read.rws.add_northsea() first." @@ -721,7 +739,7 @@ def ds_contains( if "time" not in ds.coords: msg = "time not in dataset. Run nlmod.time.set_ds_time() first." raise ValueError(msg) - + # Check if time-coord is complete time_attrs_required = ["start", "time_units"] @@ -731,6 +749,12 @@ def ds_contains( "Run nlmod.time.set_ds_time() to set time." raise ValueError(msg) + if attrs_ds: + for attr in attrs_ds_required: + if attr not in ds.attrs: + msg = f"{attr} not in dataset.attrs. Run nlmod.set_ds_attrs() first." + raise ValueError(msg) + # User-unfriendly error messages for datavar in datavars: if datavar not in ds.data_vars: From 0a6394aca637df24495a29b09373ef1c843266e4 Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Sat, 25 May 2024 11:47:07 +0200 Subject: [PATCH 39/85] Added get_exe_path with more features (#347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added get_exe_path with more features Use previously installed anywhere. If doesn't exist, download it. * Minor change to docs * test * Ensure version also with nlmod install * Update util.py * Fixes suggested by Davíd * Only kwargs allowed * update function calls to get_exe+path() * New syntax of get_exe_path compatible with prior syntax --- nlmod/dims/base.py | 2 +- nlmod/dims/grid.py | 2 +- nlmod/modpath/modpath.py | 2 +- nlmod/sim/sim.py | 2 +- nlmod/util.py | 300 ++++++++++++++++++++++++++++++++++----- 5 files changed, 268 insertions(+), 40 deletions(-) diff --git a/nlmod/dims/base.py b/nlmod/dims/base.py index 3ec3f946..43085b10 100644 --- a/nlmod/dims/base.py +++ b/nlmod/dims/base.py @@ -46,7 +46,7 @@ def set_ds_attrs(ds, model_name, model_ws, mfversion="mf6", exe_name=None): ds.attrs["created_on"] = dt.datetime.now().strftime(fmt) if exe_name is None: - exe_name = util.get_exe_path(mfversion) + exe_name = util.get_exe_path(exe_name=mfversion) ds.attrs["exe_name"] = exe_name diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 68a4c9b9..84656692 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -442,7 +442,7 @@ def refine( logger.info("create vertex grid using gridgen") if exe_name is None: - exe_name = util.get_exe_path("gridgen") + exe_name = util.get_exe_path(exe_name="gridgen") if model_ws is None: model_ws = os.path.join(ds.model_ws, "gridgen") diff --git a/nlmod/modpath/modpath.py b/nlmod/modpath/modpath.py index 125c3aa3..93d5358b 100644 --- a/nlmod/modpath/modpath.py +++ b/nlmod/modpath/modpath.py @@ -217,7 +217,7 @@ def mpf(gwf, exe_name=None, modelname=None, model_ws=None): # get executable if exe_name is None: - exe_name = util.get_exe_path("mp7_2_002_provisional") + exe_name = util.get_exe_path(exe_name="mp7_2_002_provisional") # create mpf model mpf = flopy.modpath.Modpath7( diff --git a/nlmod/sim/sim.py b/nlmod/sim/sim.py index 119a97bf..a690d038 100644 --- a/nlmod/sim/sim.py +++ b/nlmod/sim/sim.py @@ -131,7 +131,7 @@ def sim(ds, exe_name=None): logger.info("creating mf6 SIM") if exe_name is None: - exe_name = util.get_exe_path(ds.mfversion) + exe_name = util.get_exe_path(exe_name=ds.mfversion) # Create the Flopy simulation object sim = flopy.mf6.MFSimulation( diff --git a/nlmod/util.py b/nlmod/util.py index a2c1c72b..e617f2eb 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -1,11 +1,14 @@ +import json import logging import os import re import sys +from pathlib import Path import warnings from typing import Dict, Optional -import flopy +from flopy.utils import get_modflow +from flopy.utils.get_modflow import flopy_appdata_path, get_release import geopandas as gpd import requests import xarray as xr @@ -14,6 +17,8 @@ logger = logging.getLogger(__name__) +nlmod_bindir = Path(__file__).parent / "bin" + class LayerError(Exception): """Generic error when modifying layers.""" @@ -89,29 +94,276 @@ def get_model_dirs(model_ws): return figdir, cachedir -def get_exe_path(exe_name="mf6"): - """Get the full path of the executable. Uses the bin directory in the nlmod package. +def get_exe_path( + exe_name="mf6", + bindir=None, + download_if_not_found=True, + version_tag="latest", + repo="executables", + enable_version_check=True, +): + """Get the full path of the executable. + + Searching for the executables is done in the following order: + 1. The directory specified with `bindir`. Raises error if exe_name is provided + and not found. Requires enable_version_check to be False. + 2. The directory used by nlmod installed in this environment. + 3. If the executables were downloaded with flopy/nlmod from an other env, + most recent installation location of MODFLOW is found in flopy metadata + + Else: + 4. Download the executables using `version_tag` and `repo`. + + The returned directory is checked to contain exe_name if it is provided. Parameters ---------- exe_name : str, optional - name of the executable. The default is 'mf6'. + The name of the executable, by default "mf6". + bindir : Path, optional + The directory where the executables are stored, by default None + download_if_not_found : bool, optional + Download the executables if they are not found, by default True. + repo : str, default "executables" + Name of GitHub repository. Choose one of "executables" (default), + "modflow6", or "modflow6-nightly-build". + version_tag : str, default "latest" + GitHub release ID. + enable_version_check : bool, default True + If False, the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. Returns ------- - exe_path : str + exe_full_path : str full path of the executable. """ - exe_path = os.path.join(os.path.dirname(__file__), "bin", exe_name) - if sys.platform.startswith("win"): - exe_path += ".exe" + if sys.platform.startswith("win") and not exe_name.endswith(".exe"): + exe_name += ".exe" + + exe_full_path = str( + get_bin_directory( + exe_name=exe_name, + bindir=bindir, + download_if_not_found=download_if_not_found, + version_tag=version_tag, + repo=repo, + enable_version_check=enable_version_check, + ) + / exe_name + ) + + msg = f"Executable path: {exe_full_path}" + logger.debug(msg) + + return exe_full_path + + +def get_bin_directory( + exe_name="mf6", + bindir=None, + download_if_not_found=True, + version_tag="latest", + repo="executables", + enable_version_check=True, +) -> Path: + """ + Get the directory where the executables are stored. - if not os.path.exists(exe_path): - logger.warning( - f"executable {exe_path} not found, download the binaries using nlmod.util.download_mfbinaries" + Searching for the executables is done in the following order: + 1. The directory specified with `bindir`. Raises error if exe_name is provided + and not found. Requires enable_version_check to be False. + 2. The directory used by nlmod installed in this environment. + 3. If the executables were downloaded with flopy/nlmod from an other env, + most recent installation location of MODFLOW is found in flopy metadata + + Else: + 4. Download the executables using `version_tag` and `repo`. + + The returned directory is checked to contain exe_name if exe_name is provided. If exe_name + is set to None only the existence of the directory is checked. + + Parameters + ---------- + exe_name : str, optional + The name of the executable, by default None. + bindir : Path, optional + The directory where the executables are stored, by default "mf6". + download_if_not_found : bool, optional + Download the executables if they are not found, by default True. + repo : str, default "executables" + Name of GitHub repository. Choose one of "executables" (default), + "modflow6", or "modflow6-nightly-build". Used only if download is needed. + version_tag : str, default "latest" + GitHub release ID. Used only if download is needed. + enable_version_check : bool, default True + If True, the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. + + Returns + ------- + Path + The directory where the executables are stored. + + Raises + ------ + FileNotFoundError + If the executables are not found in the specified directories. + """ + bindir = Path(bindir) if bindir is not None else None + + if sys.platform.startswith("win") and not exe_name.endswith(".exe"): + exe_name += ".exe" + + # If bindir is provided + if bindir is not None and enable_version_check: + msg = "Incompatible arguments. If bindir is provided, enable_version_check should be False." + raise ValueError(msg) + + use_bindir = ( + bindir is not None and exe_name is not None and (bindir / exe_name).exists() + ) + use_bindir |= bindir is not None and exe_name is None and bindir.exists() + + if use_bindir: + return bindir + + # If the executables are in the flopy directory + flopy_bindirs = get_flopy_bin_directories( + version_tag=version_tag, repo=repo, enable_version_check=enable_version_check + ) + + if exe_name is not None: + flopy_bindirs = [ + flopy_bindir + for flopy_bindir in flopy_bindirs + if Path(flopy_bindir / exe_name).exists() + ] + else: + flopy_bindirs = [ + flopy_bindir + for flopy_bindir in flopy_bindirs + if Path(flopy_bindir).exists() + ] + + if nlmod_bindir in flopy_bindirs: + return nlmod_bindir + + if flopy_bindirs: + # Get most recent directory + return flopy_bindirs[-1] + + # Else download the executables + if download_if_not_found: + download_mfbinaries(bindir=bindir, version_tag=version_tag, repo=repo) + + # Check if the executables are in the flopy directory (or rerun this function) + return get_bin_directory( + exe_name=exe_name, + bindir=bindir, + download_if_not_found=False, + version_tag=version_tag, + repo=repo, + enable_version_check=enable_version_check, ) - return exe_path + else: + msg = f"Could not find {exe_name} in {bindir}, {nlmod_bindir} and {flopy_bindirs}." + raise FileNotFoundError(msg) + + +def get_flopy_bin_directories( + version_tag="latest", repo="executables", enable_version_check=True +): + """Get the directories where the executables are stored. + + Obtain the bin directory installed with flopy. If enable_version_check is True, + all installation location of MODFLOW are found in flopy metadata that respects + `version_tag` and `repo`. + + Parameters + ---------- + version_tag : str, default "latest" + GitHub release ID. Used only if download is needed. + repo : str, default "executables" + Name of GitHub repository. Choose one of "executables" (default), + "modflow6", or "modflow6-nightly-build". Used only if download is needed. + enable_version_check : bool, default False + If False, the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. + + Returns + ------- + list + list of directories where the executables are stored. + """ + flopy_metadata_fp = flopy_appdata_path / "get_modflow.json" + + if not flopy_metadata_fp.exists(): + return [] + + meta_raw = flopy_metadata_fp.read_text() + + # Remove trailing characters that are not part of the JSON. + while meta_raw[-3:] != "}\n]": + meta_raw = meta_raw[:-1] + + # Get metadata of all flopy installations + meta_list = json.loads(meta_raw) + + # To convert latest into an explicit tag + if enable_version_check: + version_tag_pin = get_release(tag=version_tag, repo=repo, quiet=True)[ + "tag_name" + ] + + # get path to the most recent installation. Appended to end of get_modflow.json + meta_list_validversion = [ + meta + for meta in meta_list + if (meta["release_id"] == version_tag_pin) and (meta["repo"] == repo) + ] + + else: + meta_list_validversion = meta_list + + path_list = [ + Path(meta["bindir"]) + for meta in meta_list_validversion + if Path(meta["bindir"]).exists() + ] + return path_list + + +def download_mfbinaries(bindir=None, version_tag="latest", repo="executables"): + """Download and unpack platform-specific modflow binaries. + + Source: USGS + + Parameters + ---------- + binpath : str, optional + path to directory to download binaries to, if it doesnt exist it + is created. Default is None which sets dir to nlmod/bin. + repo : str, default "executables" + Name of GitHub repository. Choose one of "executables" (default), + "modflow6", or "modflow6-nightly-build". + version_tag : str, default "latest" + GitHub release ID. + + """ + if bindir is None: + # Path objects are immutable so a copy is implied + bindir = nlmod_bindir + + if not os.path.isdir(bindir): + os.makedirs(bindir) + + get_modflow(bindir=str(bindir), release_id=version_tag, repo=repo) + + if sys.platform.startswith("win"): + # download the provisional version of modpath from Github + download_modpath_provisional_exe(bindir) def get_ds_empty(ds, keep_coords=None): @@ -430,30 +682,6 @@ def save_response_content(response, destination): save_response_content(response, destination) -def download_mfbinaries(bindir=None): - """Download and unpack platform-specific modflow binaries. - - Source: USGS - - Parameters - ---------- - binpath : str, optional - path to directory to download binaries to, if it doesnt exist it - is created. Default is None which sets dir to nlmod/bin. - version : str, optional - version string, by default 8.0 - """ - - if bindir is None: - bindir = os.path.join(os.path.dirname(__file__), "bin") - if not os.path.isdir(bindir): - os.makedirs(bindir) - flopy.utils.get_modflow(bindir) - if sys.platform.startswith("win"): - # download the provisional version of modpath from Github - download_modpath_provisional_exe(bindir) - - def download_modpath_provisional_exe(bindir=None, timeout=120): """Download the provisional version of modpath to the folder with binaries.""" if bindir is None: From c78976eced955c3ab719bfd27759ff61544c1a6a Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Wed, 29 May 2024 10:29:02 +0200 Subject: [PATCH 40/85] Github token needed for CI when downloading executables (#349) * Github token needed for CI when downloading executables * Envvar NLMOD_SUPPRESS_VERION_CHECK * Renamed envvar * Let's see if we can now get by without passing the GITHUB_TOKEN env * Default to not check version * Consume version_tag arg when creating model_ds * Removing env SUPPRESS_EXE_VERSION_CHECK * Removed dangling argument * Added version_tag to refine/gridgen * Also auto-download modpath_provisional for other platforms than win --- nlmod/dims/base.py | 23 ++++++- nlmod/dims/grid.py | 10 ++- nlmod/modpath/modpath.py | 4 +- nlmod/sim/sim.py | 28 ++++++-- nlmod/util.py | 138 +++++++++++++++++++++++++-------------- 5 files changed, 144 insertions(+), 59 deletions(-) diff --git a/nlmod/dims/base.py b/nlmod/dims/base.py index 43085b10..e932107a 100644 --- a/nlmod/dims/base.py +++ b/nlmod/dims/base.py @@ -14,7 +14,9 @@ logger = logging.getLogger(__name__) -def set_ds_attrs(ds, model_name, model_ws, mfversion="mf6", exe_name=None): +def set_ds_attrs( + ds, model_name, model_ws, mfversion="mf6", exe_name=None, version_tag=None +): """Set the attribute of a model dataset. Parameters @@ -31,6 +33,11 @@ def set_ds_attrs(ds, model_name, model_ws, mfversion="mf6", exe_name=None): path to modflow executable, default is None, which assumes binaries are available in nlmod/bin directory. Binaries can be downloaded using `nlmod.util.download_mfbinaries()`. + version_tag : str, default None + GitHub release ID: for example "18.0" or "latest". If version_tag is provided, + the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag`. If not found, the executables are downloaded. + Not compatible with exe_name. Returns ------- @@ -46,7 +53,9 @@ def set_ds_attrs(ds, model_name, model_ws, mfversion="mf6", exe_name=None): ds.attrs["created_on"] = dt.datetime.now().strftime(fmt) if exe_name is None: - exe_name = util.get_exe_path(exe_name=mfversion) + exe_name = util.get_exe_path(exe_name=mfversion, version_tag=version_tag) + else: + exe_name = util.get_exe_path(exe_name=exe_name, version_tag=version_tag) ds.attrs["exe_name"] = exe_name @@ -78,6 +87,7 @@ def to_model_ds( drop_attributes=True, transport=False, remove_nan_layers=True, + version_tag=None, ): """Transform an input dataset to a groundwater model dataset. @@ -136,6 +146,11 @@ def to_model_ds( remove_nan_layers : bool, optional if True remove layers with only nan values in the botm. Default is True. + version_tag : str, default None + GitHub release ID: for example "18.0" or "latest". If version_tag is provided, + the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag`. If not found, the executables are downloaded. + Not compatible with exe_name. Returns ------- @@ -176,7 +191,9 @@ def to_model_ds( ds = extrapolate_ds(ds) # add attributes - ds = set_ds_attrs(ds, model_name, model_ws) + ds = set_ds_attrs( + ds, model_name, model_ws, mfversion="mf6", version_tag=version_tag + ) ds.attrs["transport"] = int(transport) # fill nan's diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 84656692..5dac18b8 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -406,6 +406,7 @@ def refine( exe_name=None, remove_nan_layers=True, model_coordinates=False, + version_tag=None, ): """Refine the grid (discretization by vertices, disv), using Gridgen. @@ -432,6 +433,11 @@ def refine( When model_coordinates is True, the features supplied in refinement_features are already in model-coordinates. Only used when a grid is rotated. The default is False. + version_tag : str, default None + GitHub release ID: for example "18.0" or "latest". If version_tag is provided, + the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag`. If not found, the executables are downloaded. + Not compatible with exe_name. Returns ------- @@ -442,7 +448,9 @@ def refine( logger.info("create vertex grid using gridgen") if exe_name is None: - exe_name = util.get_exe_path(exe_name="gridgen") + exe_name = util.get_exe_path(exe_name="gridgen", version_tag=version_tag) + else: + exe_name = util.get_exe_path(exe_name=exe_name, version_tag=version_tag) if model_ws is None: model_ws = os.path.join(ds.model_ws, "gridgen") diff --git a/nlmod/modpath/modpath.py b/nlmod/modpath/modpath.py index 93d5358b..336db050 100644 --- a/nlmod/modpath/modpath.py +++ b/nlmod/modpath/modpath.py @@ -215,9 +215,11 @@ def mpf(gwf, exe_name=None, modelname=None, model_ws=None): "the save_flows option of the npf package should be True not None" ) - # get executable + # get executable. version_tag not supported yet if exe_name is None: exe_name = util.get_exe_path(exe_name="mp7_2_002_provisional") + else: + exe_name = util.get_exe_path(exe_name=exe_name) # create mpf model mpf = flopy.modpath.Modpath7( diff --git a/nlmod/sim/sim.py b/nlmod/sim/sim.py index a690d038..052d2d9c 100644 --- a/nlmod/sim/sim.py +++ b/nlmod/sim/sim.py @@ -107,7 +107,7 @@ def get_tdis_perioddata(ds, nstp="nstp", tsmult="tsmult"): return tdis_perioddata -def sim(ds, exe_name=None): +def sim(ds, exe_name=None, version_tag=None): """create sim from the model dataset. Parameters @@ -117,9 +117,14 @@ def sim(ds, exe_name=None): attributes: model_name, mfversion, model_ws, time_units, start, perlen, nstp, tsmult exe_name: str, optional - path to modflow executable, default is None, which assumes binaries - are available in nlmod/bin directory. Binaries can be downloaded - using `nlmod.util.download_mfbinaries()`. + path to modflow executable, default is None. If None, the path is + obtained from the flopy metadata that respects `version_tag`. If not + found, the executables are downloaded. Not compatible with version_tag. + version_tag : str, default None + GitHub release ID: for example "18.0" or "latest". If version_tag is provided, + the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag`. If not found, the executables are downloaded. + Not compatible with exe_name. Returns ------- @@ -130,8 +135,19 @@ def sim(ds, exe_name=None): # start creating model logger.info("creating mf6 SIM") - if exe_name is None: - exe_name = util.get_exe_path(exe_name=ds.mfversion) + # Most likely exe_name was previously set with to_model_ds() + if exe_name is not None: + exe_name = util.get_exe_path(exe_name=exe_name, version_tag=version_tag) + elif "exe_name" in ds.attrs: + exe_name = util.get_exe_path( + exe_name=ds.attrs["exe_name"], version_tag=version_tag + ) + elif "mfversion" in ds.attrs: + exe_name = util.get_exe_path( + exe_name=ds.attrs["mfversion"], version_tag=version_tag + ) + else: + raise ValueError("No exe_name provided and no exe_name found in ds.attrs") # Create the Flopy simulation object sim = flopy.mf6.MFSimulation( diff --git a/nlmod/util.py b/nlmod/util.py index e617f2eb..16c65a6d 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -98,13 +98,13 @@ def get_exe_path( exe_name="mf6", bindir=None, download_if_not_found=True, - version_tag="latest", + version_tag=None, repo="executables", - enable_version_check=True, ): """Get the full path of the executable. Searching for the executables is done in the following order: + 0. If exe_name is a full path, return the full path of the executable. 1. The directory specified with `bindir`. Raises error if exe_name is provided and not found. Requires enable_version_check to be False. 2. The directory used by nlmod installed in this environment. @@ -126,12 +126,15 @@ def get_exe_path( Download the executables if they are not found, by default True. repo : str, default "executables" Name of GitHub repository. Choose one of "executables" (default), - "modflow6", or "modflow6-nightly-build". - version_tag : str, default "latest" - GitHub release ID. - enable_version_check : bool, default True - If False, the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. + "modflow6", or "modflow6-nightly-build". If repo and version_tag are + provided the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. If not found, the executables are downloaded + using repo and version_tag. + version_tag : str, default None + GitHub release ID: for example "18.0" or "latest". If repo and version_tag are + provided the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. If not found, the executables are downloaded + using repo and version_tag. Returns ------- @@ -141,17 +144,29 @@ def get_exe_path( if sys.platform.startswith("win") and not exe_name.endswith(".exe"): exe_name += ".exe" - exe_full_path = str( - get_bin_directory( - exe_name=exe_name, - bindir=bindir, - download_if_not_found=download_if_not_found, - version_tag=version_tag, - repo=repo, - enable_version_check=enable_version_check, + # If exe_name is a full path + if Path(exe_name).exists(): + enable_version_check = version_tag is not None and repo is not None + + if enable_version_check: + msg = ( + "Incompatible arguments. If exe_name is provided, unable to check " + "the version." + ) + raise ValueError(msg) + exe_full_path = exe_name + + else: + exe_full_path = str( + get_bin_directory( + exe_name=exe_name, + bindir=bindir, + download_if_not_found=download_if_not_found, + version_tag=version_tag, + repo=repo, + ) + / exe_name ) - / exe_name - ) msg = f"Executable path: {exe_full_path}" logger.debug(msg) @@ -163,14 +178,14 @@ def get_bin_directory( exe_name="mf6", bindir=None, download_if_not_found=True, - version_tag="latest", + version_tag=None, repo="executables", - enable_version_check=True, ) -> Path: """ Get the directory where the executables are stored. Searching for the executables is done in the following order: + 0. If exe_name is a full path, return the full path of the executable. 1. The directory specified with `bindir`. Raises error if exe_name is provided and not found. Requires enable_version_check to be False. 2. The directory used by nlmod installed in this environment. @@ -193,12 +208,15 @@ def get_bin_directory( Download the executables if they are not found, by default True. repo : str, default "executables" Name of GitHub repository. Choose one of "executables" (default), - "modflow6", or "modflow6-nightly-build". Used only if download is needed. - version_tag : str, default "latest" - GitHub release ID. Used only if download is needed. - enable_version_check : bool, default True - If True, the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. + "modflow6", or "modflow6-nightly-build". If repo and version_tag are + provided the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. If not found, the executables are downloaded + using repo and version_tag. + version_tag : str, default None + GitHub release ID: for example "18.0" or "latest". If repo and version_tag are + provided the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. If not found, the executables are downloaded + using repo and version_tag. Returns ------- @@ -215,9 +233,21 @@ def get_bin_directory( if sys.platform.startswith("win") and not exe_name.endswith(".exe"): exe_name += ".exe" + enable_version_check = version_tag is not None and repo is not None + + # If exe_name is a full path + if Path(exe_name).exists(): + if enable_version_check: + msg = ( + "Incompatible arguments. If exe_name is provided, unable to check " + "the version." + ) + raise ValueError(msg) + return Path(exe_name).parent + # If bindir is provided if bindir is not None and enable_version_check: - msg = "Incompatible arguments. If bindir is provided, enable_version_check should be False." + msg = "Incompatible arguments. If bindir is provided, unable to check the version." raise ValueError(msg) use_bindir = ( @@ -229,9 +259,7 @@ def get_bin_directory( return bindir # If the executables are in the flopy directory - flopy_bindirs = get_flopy_bin_directories( - version_tag=version_tag, repo=repo, enable_version_check=enable_version_check - ) + flopy_bindirs = get_flopy_bin_directories(version_tag=version_tag, repo=repo) if exe_name is not None: flopy_bindirs = [ @@ -257,14 +285,13 @@ def get_bin_directory( if download_if_not_found: download_mfbinaries(bindir=bindir, version_tag=version_tag, repo=repo) - # Check if the executables are in the flopy directory (or rerun this function) + # Rerun this function return get_bin_directory( exe_name=exe_name, bindir=bindir, download_if_not_found=False, version_tag=version_tag, repo=repo, - enable_version_check=enable_version_check, ) else: @@ -272,9 +299,7 @@ def get_bin_directory( raise FileNotFoundError(msg) -def get_flopy_bin_directories( - version_tag="latest", repo="executables", enable_version_check=True -): +def get_flopy_bin_directories(version_tag=None, repo="executables"): """Get the directories where the executables are stored. Obtain the bin directory installed with flopy. If enable_version_check is True, @@ -283,14 +308,17 @@ def get_flopy_bin_directories( Parameters ---------- - version_tag : str, default "latest" - GitHub release ID. Used only if download is needed. repo : str, default "executables" Name of GitHub repository. Choose one of "executables" (default), - "modflow6", or "modflow6-nightly-build". Used only if download is needed. - enable_version_check : bool, default False - If False, the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. + "modflow6", or "modflow6-nightly-build". If repo and version_tag are + provided the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. If not found, the executables are downloaded + using repo and version_tag. + version_tag : str, default None + GitHub release ID: for example "18.0" or "latest". If repo and version_tag are + provided the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. If not found, the executables are downloaded + using repo and version_tag. Returns ------- @@ -311,11 +339,21 @@ def get_flopy_bin_directories( # Get metadata of all flopy installations meta_list = json.loads(meta_raw) - # To convert latest into an explicit tag + enable_version_check = version_tag is not None and repo is not None + if enable_version_check: - version_tag_pin = get_release(tag=version_tag, repo=repo, quiet=True)[ - "tag_name" - ] + msg = ( + "The version of the executables will be checked, because the " + f"`version_tag={version_tag}` is passed to `get_flopy_bin_directories()`." + ) + + # To convert latest into an explicit tag + if version_tag == "latest": + version_tag_pin = get_release(tag=version_tag, repo=repo, quiet=True)[ + "tag_name" + ] + else: + version_tag_pin = version_tag # get path to the most recent installation. Appended to end of get_modflow.json meta_list_validversion = [ @@ -325,7 +363,12 @@ def get_flopy_bin_directories( ] else: + msg = ( + "The version of the executables will not be checked, because the " + "`version_tag` is not passed to `get_flopy_bin_directories()`." + ) meta_list_validversion = meta_list + logger.info(msg) path_list = [ Path(meta["bindir"]) @@ -361,9 +404,8 @@ def download_mfbinaries(bindir=None, version_tag="latest", repo="executables"): get_modflow(bindir=str(bindir), release_id=version_tag, repo=repo) - if sys.platform.startswith("win"): - # download the provisional version of modpath from Github - download_modpath_provisional_exe(bindir) + # download the provisional version of modpath from Github + download_modpath_provisional_exe(bindir=bindir, timeout=120) def get_ds_empty(ds, keep_coords=None): From 82b8e446f11d3fcd8c0b92cd75622a574d588f25 Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Mon, 10 Jun 2024 16:19:02 +0200 Subject: [PATCH 41/85] add animate method to cross section dataset class --- nlmod/plot/dcs.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index 52fb7fde..47415b25 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -1,9 +1,12 @@ import flopy +import logging import matplotlib import matplotlib.pyplot as plt import numpy as np import pandas as pd import xarray as xr +from functools import partial +from matplotlib.animation import FFMpegWriter, FuncAnimation from matplotlib.collections import LineCollection, PatchCollection from matplotlib.patches import Rectangle from shapely.affinity import affine_transform @@ -12,6 +15,8 @@ from ..dims.grid import modelgrid_from_ds from ..dims.resample import get_affine_world_to_mod +logger = logging.getLogger(__name__) + class DatasetCrossSection: # assumes: @@ -322,6 +327,51 @@ def plot_grid( self.ax.add_collection(patch_collection) return patch_collection + def get_patches_array(self, z): + """similar to plot_array function, only computes the array to update an existing plot_array. + + Parameters + ---------- + z : DataArray + data to plot on the patches. + + Returns + ------- + list + plot data. + """ + if isinstance(z, xr.DataArray): + z = z.data + + if self.icell2d in self.ds.dims: + assert len(z.shape) == 2 + assert z.shape[0] == len(self.layer) + assert z.shape[1] == len(self.ds[self.icell2d]) + + zcs = z[:, self.icell2ds] + else: + assert len(z.shape) == 3 + assert z.shape[0] == len(self.layer) + assert z.shape[1] == len(self.ds[self.y]) + assert z.shape[2] == len(self.ds[self.x]) + + zcs = z[:, self.rows, self.cols] + + array = [] + for i in range(zcs.shape[0]): + for j in range(zcs.shape[1]): + if not ( + np.isnan(self.top[i, j]) + or np.isnan(self.bot[i, j]) + or np.isnan(zcs[i, j]) + ): + if self.bot[i, j] == self.zmax or self.top[i, j] == self.zmin: + continue + + array.append(zcs[i, j]) + + return array + def plot_array(self, z, head=None, **kwargs): if isinstance(z, xr.DataArray): z = z.data @@ -415,3 +465,69 @@ def get_top_and_bot(self, top, bot): top[top > self.zmax] = self.zmax bot[bot > self.zmax] = self.zmax return top, bot + + def animate( + self, + da, + cmap="Spectral_r", + norm=None, + head=None, + plot_title="", + date_fmt="%Y-%m-%d", + cbar_label=None, + fname=None, + ): + """animate a cross section""" + + f = self.ax.get_figure() + + # plot first timeframe + iper = 0 + if head is not None: + plot_head = head[iper].values + logger.info("varying head not supported for animation yet") + + pc = self.plot_array(da[iper].squeeze(), cmap=cmap, norm=norm, head=plot_head) + cbar = f.colorbar(pc, ax=self.ax, shrink=1.0) + + if cbar_label is not None: + cbar.set_label(cbar_label) + elif "units" in da.attrs: + cbar.set_label(da.units) + + t = pd.Timestamp(da.time.values[iper]) + title = self.ax.set_title(f"{plot_title}, t = {t.strftime(date_fmt)}") + + # update func + def update(iper, pc, title): + array = self.get_patches_array(da[iper].squeeze()) + pc.set_array(array) + + # update title + t = pd.Timestamp(da.time.values[iper]) + title.set_text(f"{plot_title}, t = {t.strftime(date_fmt)}") + + return pc, title + + # create animation + anim = FuncAnimation( + f, + partial(update, pc=pc, title=title), + frames=da["time"].shape[0], + blit=False, + interval=100, + ) + + # save animation + if fname is None: + return anim + else: + # save animation as mp4 + writer = FFMpegWriter( + fps=10, + bitrate=-1, + extra_args=["-pix_fmt", "yuv420p"], + codec="libx264", + ) + anim.save(fname, writer=writer) + return anim From f48d1159a22d5d80aeb5549c9bec7f0ac3df5f4a Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Mon, 10 Jun 2024 16:19:18 +0200 Subject: [PATCH 42/85] update docstring to include boundname --- nlmod/gwf/surface_water.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index 07d7c60c..94bc426b 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -366,6 +366,7 @@ def build_spd( celldata : geopandas.GeoDataFrame GeoDataFrame containing data. Cellid must be the index, and must have columns "rbot", "stage" and "cond". + Optional columns are 'boundname' and 'aux'. These columns should have type str. pkg : str Modflow package: RIV, DRN or GHB ds : xarray.DataSet From 9aa73f415f54be5b0ac78c15ff706c60da8a97d8 Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Tue, 11 Jun 2024 12:49:58 +0200 Subject: [PATCH 43/85] improve animate and add plot_map_cs method --- nlmod/plot/dcs.py | 85 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index 47415b25..383aa68c 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -1,10 +1,13 @@ import flopy import logging import matplotlib + +import geopandas as gpd import matplotlib.pyplot as plt import numpy as np import pandas as pd import xarray as xr + from functools import partial from matplotlib.animation import FFMpegWriter, FuncAnimation from matplotlib.collections import LineCollection, PatchCollection @@ -14,6 +17,8 @@ from ..dims.grid import modelgrid_from_ds from ..dims.resample import get_affine_world_to_mod +from .plotutil import get_map + logger = logging.getLogger(__name__) @@ -327,6 +332,49 @@ def plot_grid( self.ax.add_collection(patch_collection) return patch_collection + def plot_map_cs( + self, + ax=None, + figsize=5, + background=True, + lw=5, + ls="--", + label="cross section", + **kwargs, + ): + """Creates a different figure with the map of the cross section + + Parameters + ---------- + ax : None or matplotlib.Axes, optional + if None a new axis object is created using nlmod.plot.get_map() + figsize : int, optional + size of the figure, only used if ax is None, by default 5 + background : bool, optional + add a backgroun map, only used if ax is None, by default True + lw : int, optional + linewidth of the cross section, by default 10 + ls : str, optional + linestyle of the cross section, by default "--" + label : str, optional + label of the cross section, by default "cross section" + **kwargs are passed to the nlmod.plot.get_map() function. Only if ax is None + + Returns + ------- + matplotlib Axes + axes + """ + + if ax is None: + f, ax = get_map( + self.ds.extent, background=background, figsize=figsize, **kwargs + ) + gpd.GeoDataFrame(geometry=[self.line]).plot(ax=ax, ls=ls, lw=lw, label=label) + ax.legend() + + return ax + def get_patches_array(self, z): """similar to plot_array function, only computes the array to update an existing plot_array. @@ -477,7 +525,34 @@ def animate( cbar_label=None, fname=None, ): - """animate a cross section""" + """animate a cross section + + Parameters + ---------- + da : DataArray + should have dimensions structured: time, y, x or vertex: time, icell2d + cmap : str, optional + passed to plot_array function, by default "Spectral_r" + norm : , optional + norm for the colorbar of the datarray, by default None + head : DataArray, optional + If not given the top cell is completely filled, by default None + plot_title : str or None, optional + if not None a title is added which is updated with every timestep (using + date_fmt for the date format), by default "" + date_fmt : str, optional + date format for plot title, by default "%Y-%m-%d" + cbar_label : str, optional + label for the colorbar, by default None + fname : str or Path, optional + filename if not None this is where the aniation is saved as mp4, by + default None + + Returns + ------- + matplotlib.animation.FuncAnimation + animation object + """ f = self.ax.get_figure() @@ -496,7 +571,10 @@ def animate( cbar.set_label(da.units) t = pd.Timestamp(da.time.values[iper]) - title = self.ax.set_title(f"{plot_title}, t = {t.strftime(date_fmt)}") + if plot_title is None: + title = None + else: + title = self.ax.set_title(f"{plot_title}, t = {t.strftime(date_fmt)}") # update func def update(iper, pc, title): @@ -505,7 +583,8 @@ def update(iper, pc, title): # update title t = pd.Timestamp(da.time.values[iper]) - title.set_text(f"{plot_title}, t = {t.strftime(date_fmt)}") + if title is not None: + title.set_text(f"{plot_title}, t = {t.strftime(date_fmt)}") return pc, title From d252ce83e6a6513967e19ca6cba74e47c77236ce Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Tue, 11 Jun 2024 12:52:41 +0200 Subject: [PATCH 44/85] add examples to notebooks --- docs/examples/03_local_grid_refinement.ipynb | 62 +++++++++++++++++- docs/examples/09_schoonhoven.ipynb | 69 ++++++++++++++++++-- 2 files changed, 122 insertions(+), 9 deletions(-) diff --git a/docs/examples/03_local_grid_refinement.ipynb b/docs/examples/03_local_grid_refinement.ipynb index 60e5606f..f2326d50 100644 --- a/docs/examples/03_local_grid_refinement.ipynb +++ b/docs/examples/03_local_grid_refinement.ipynb @@ -23,6 +23,7 @@ "import os\n", "\n", "import flopy\n", + "import numpy as np\n", "import pandas as pd\n", "import geopandas as gpd\n", "import hydropandas as hpd\n", @@ -307,7 +308,50 @@ "nlmod.plot.data_array(ds[\"bathymetry\"], ds, ax=axes[0][0])\n", "nlmod.plot.data_array(ds[\"northsea\"], ds, ax=axes[0][1])\n", "nlmod.plot.data_array(ds[\"kh\"][1], ds, ax=axes[1][0])\n", - "nlmod.plot.data_array(ds[\"recharge\"][0], ds, ax=axes[1][1]);" + "nlmod.plot.data_array(ds[\"recharge\"][0], ds, ax=axes[1][1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There is also the option to create an animation of a cross section" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = np.mean(extent[:2])\n", + "line = [(x, extent[2]), (x, extent[3])]\n", + "\n", + "\n", + "ds[\"heads\"] = nlmod.gwf.get_heads_da(ds)\n", + "\n", + "f, ax = plt.subplots(figsize=(10, 6))\n", + "dcs = nlmod.plot.DatasetCrossSection(ds, line, ax=ax, zmin=-30.0, zmax=5.0)\n", + "\n", + "# add labels with layer names\n", + "ax.set_xlabel(\"distance [m]\")\n", + "ax.set_ylabel(\"elevation [mNAP]\")\n", + "\n", + "dcs.plot_grid(lw=0.25, edgecolor=\"k\", alpha=0.5, vertical=False)\n", + "dcs.plot_layers(alpha=0.0, min_label_area=5e4)\n", + "dcs.plot_surface(ds[\"top\"], lw=1.0, color=\"k\")\n", + "\n", + "fname = os.path.join(ds.figdir, f\"anim_xsec_x{int(x)}_head.mp4\")\n", + "dcs.animate(\n", + " ds[\"heads\"],\n", + " cmap=\"Spectral_r\",\n", + " head=ds[\"heads\"],\n", + " plot_title=f\"doorsnede at x={int(x)}\",\n", + " date_fmt=\"%Y-%m-%d\",\n", + " fname=fname,\n", + ")\n", + "\n", + "dcs.plot_map_cs(lw=5)" ] }, { @@ -398,8 +442,22 @@ } ], "metadata": { + "kernelspec": { + "display_name": "nlmod", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.4" } }, "nbformat": 4, diff --git a/docs/examples/09_schoonhoven.ipynb b/docs/examples/09_schoonhoven.ipynb index aceedd44..e107c82d 100644 --- a/docs/examples/09_schoonhoven.ipynb +++ b/docs/examples/09_schoonhoven.ipynb @@ -339,8 +339,8 @@ "].isin(ids_oude_haven)\n", "lakes = bgt_grid[mask].copy()\n", "lakes[\"name\"] = \"\"\n", - "lakes.loc[lakes[\"identificatie\"].isin(ids_grote_gracht), \"name\"] = \"grote gracht\"\n", - "lakes.loc[lakes[\"identificatie\"].isin(ids_oude_haven), \"name\"] = \"oude haven\"\n", + "lakes.loc[lakes[\"identificatie\"].isin(ids_grote_gracht), \"name\"] = \"grotegracht\"\n", + "lakes.loc[lakes[\"identificatie\"].isin(ids_oude_haven), \"name\"] = \"oudehaven\"\n", "bgt_grid = bgt_grid[~mask]" ] }, @@ -421,9 +421,9 @@ "\n", "# add outlet to Oude Haven, water flows from Oude Haven to Grote Gracht.\n", "lakes.loc[lakes[\"identificatie\"].isin(ids_oude_haven), \"lakeout\"] = 0\n", - "lakes.loc[\n", - " lakes[\"identificatie\"].isin(ids_oude_haven), \"outlet_invert\"\n", - "] = 1.0 # overstort hoogte\n", + "lakes.loc[lakes[\"identificatie\"].isin(ids_oude_haven), \"outlet_invert\"] = (\n", + " 1.0 # overstort hoogte\n", + ")\n", "\n", "# add lake to groundwaterflow model\n", "nlmod.gwf.lake_from_gdf(gwf, lakes, ds, boundname_column=\"name\")" @@ -503,7 +503,7 @@ " colorbar_label=\"head [m NAP]\",\n", " title=\"mean head\",\n", ")\n", - "bgt.dissolve().plot(ax=pc.axes, edgecolor=\"k\", facecolor=\"none\");" + "bgt.dissolve().plot(ax=pc.axes, edgecolor=\"k\", facecolor=\"none\")" ] }, { @@ -535,6 +535,47 @@ "f.tight_layout(pad=0.0)" ] }, + { + "cell_type": "markdown", + "id": "01ddcb4f", + "metadata": {}, + "source": [ + "### Animate a cross-section with heads" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e195442", + "metadata": {}, + "outputs": [], + "source": [ + "# x = np.mean(extent[:2])\n", + "# line = [(x, extent[2]), (x, extent[3])]\n", + "\n", + "# f, ax = plt.subplots(figsize=(10, 6))\n", + "# norm\n", + "# dcs = DatasetCrossSection(ds, line, ax=ax, zmin=-100.0, zmax=10.0)\n", + "\n", + "# # add labels with layer names\n", + "# ax.set_xlabel(\"distance [m]\")\n", + "# ax.set_ylabel(\"elevation [mNAP]\")\n", + "\n", + "# dcs.plot_grid(lw=0.25, edgecolor=\"k\", alpha=0.5, vertical=False)\n", + "# dcs.plot_layers(alpha=0.0, min_label_area=5e4)\n", + "# dcs.plot_surface(ds[\"top\"], lw=1.0, color=\"k\")\n", + "\n", + "# fname = os.path.join(ds.figdir, f\"anim_xsec_x{int(x)}_head.mp4\")\n", + "# dcs.animate(\n", + "# head,\n", + "# cmap=\"Spectral_r\",\n", + "# head=head,\n", + "# plot_title=f\"doorsnede at x={int(x)}\",\n", + "# date_fmt=\"%Y-%m-%d\",\n", + "# fname=fname,\n", + "# )" + ] + }, { "cell_type": "markdown", "id": "6d543af4", @@ -786,8 +827,22 @@ } ], "metadata": { + "kernelspec": { + "display_name": "nlmod", + "language": "python", + "name": "python3" + }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.4" } }, "nbformat": 4, From 491b39b41c084f39fa6636fb21734fff82dfdf4e Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Wed, 12 Jun 2024 18:20:41 +0200 Subject: [PATCH 45/85] codacy fix --- nlmod/plot/dcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index 383aa68c..f27162b0 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -367,7 +367,7 @@ def plot_map_cs( """ if ax is None: - f, ax = get_map( + _, ax = get_map( self.ds.extent, background=background, figsize=figsize, **kwargs ) gpd.GeoDataFrame(geometry=[self.line]).plot(ax=ax, ls=ls, lw=lw, label=label) From 6ff71729c176406695eba1f808c7efc322dd4cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Mon, 17 Jun 2024 09:11:53 +0200 Subject: [PATCH 46/85] Remove delr and delc from ds (#346) * Remove delr and delc from ds, first commit * import grid again in gwf * Some more removal of delr and delc and simplify _get_delr_from_x * Handle comments by @OnnoEbbens --- nlmod/cache.py | 12 +++---- nlmod/dims/base.py | 7 ---- nlmod/dims/grid.py | 42 +++++------------------- nlmod/dims/resample.py | 66 +++++++++++++++++++++++++++++++++----- nlmod/gis.py | 7 +--- nlmod/gwf/gwf.py | 15 +++------ nlmod/gwf/surface_water.py | 8 +++-- nlmod/read/rws.py | 6 ++-- 8 files changed, 84 insertions(+), 79 deletions(-) diff --git a/nlmod/cache.py b/nlmod/cache.py index 3d961f37..1aa3672f 100644 --- a/nlmod/cache.py +++ b/nlmod/cache.py @@ -703,12 +703,6 @@ def ds_contains( datavars.append("yv") datavars.append("icvert") - # TODO: temporary fix until delr/delc are completely removed - if "delr" in datavars: - datavars.remove("delr") - if "delc" in datavars: - datavars.remove("delc") - if "angrot" in ds.attrs: # set by `nlmod.base.to_model_ds()` and `nlmod.dims.resample._set_angrot_attributes()` attrs_angrot_required = ["angrot", "xorigin", "yorigin"] @@ -745,8 +739,10 @@ def ds_contains( for t_attr in time_attrs_required: if t_attr not in ds["time"].attrs: - msg = f"{t_attr} not in dataset['time'].attrs. " +\ - "Run nlmod.time.set_ds_time() to set time." + msg = ( + f"{t_attr} not in dataset['time'].attrs. " + + "Run nlmod.time.set_ds_time() to set time." + ) raise ValueError(msg) if attrs_ds: diff --git a/nlmod/dims/base.py b/nlmod/dims/base.py index e932107a..c2b729b6 100644 --- a/nlmod/dims/base.py +++ b/nlmod/dims/base.py @@ -182,8 +182,6 @@ def to_model_ds( ) elif isinstance(delr, np.ndarray) and isinstance(delc, np.ndarray): ds["area"] = ("y", "x"), np.outer(delc, delr) - ds["delr"] = ("x"), delr - ds["delc"] = ("y"), delc else: raise TypeError("unexpected type for delr and/or delc") @@ -383,11 +381,6 @@ def _get_structured_grid_ds( coords=coords, attrs=attrs, ) - # set delr and delc - delr = np.diff(xedges) - ds["delr"] = ("x"), delr - delc = -np.diff(yedges) - ds["delc"] = ("y"), delc if crs is not None: ds.rio.set_crs(crs) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 5dac18b8..2aadff70 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -40,6 +40,8 @@ get_affine_mod_to_world, get_affine_world_to_mod, structured_da_to_ds, + get_delr, + get_delc, ) logger = logging.getLogger(__name__) @@ -211,17 +213,9 @@ def modelgrid_from_ds(ds, rotated=True, nlay=None, top=None, botm=None, **kwargs raise TypeError( f"extent should be a list, tuple or numpy array, not {type(ds.extent)}" ) - if "delc" in ds: - delc = ds["delc"].values - else: - raise KeyError("delc not in dataset") - if "delr" in ds: - delr = ds["delr"].values - else: - raise KeyError("delr not in dataset") modelgrid = StructuredGrid( - delc=delc, - delr=delr, + delc=get_delc(ds), + delr=get_delr(ds), **kwargs, ) elif ds.gridtype == "vertex": @@ -463,8 +457,8 @@ def refine( gwf, nrow=len(ds.y), ncol=len(ds.x), - delr=ds.delr, - delc=ds.delc, + delr=get_delr(ds), + delc=get_delc(ds), xorigin=ds.extent[0], yorigin=ds.extent[2], ) @@ -1948,28 +1942,8 @@ def polygons_from_model_ds(model_ds): """ if model_ds.gridtype == "structured": - # TODO: update with new delr/delc calculation - # check if coordinates are consistent with delr/delc values - delr_x = np.unique(model_ds.x.values[1:] - model_ds.x.values[:-1]) - delc_y = np.unique(model_ds.y.values[:-1] - model_ds.y.values[1:]) - - delr = np.unique(model_ds.delr) - if len(delr) > 1: - raise ValueError("delr is variable") - else: - delr = delr.item() - - delc = np.unique(model_ds.delc) - if len(delc) > 1: - raise ValueError("delc is variable") - else: - delc = delc.item() - - if not ((delr_x == delr) and (delc_y == delc)): - raise ValueError( - "delr and delc attributes of model_ds inconsistent " - "with x and y coordinates" - ) + delr = get_delr(model_ds) + delc = get_delc(model_ds) xmins = model_ds.x - (delr * 0.5) xmaxs = model_ds.x + (delr * 0.5) diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index 73a13427..f3ff7fe1 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -92,6 +92,58 @@ def get_xy_mid_structured(extent, delr, delc, descending_y=True): raise TypeError("unexpected type for delr and/or delc") +def get_delr(ds): + """ + Get the distance along rows (delr) from the x-coordinate of a structured model + dataset. + + Parameters + ---------- + ds : xarray.Dataset + A model dataset containing an x-coordinate and an attribute 'extent'. + + Returns + ------- + delr : np.ndarray + The cell-size along rows (of length ncol). + + """ + assert ds.gridtype == "structured" + x = (ds.x - ds.extent[0]).values + delr = _get_delta_along_axis(x) + return delr + + +def get_delc(ds): + """ + Get the distance along columns (delc) from the y-coordinate of a structured model + dataset. + + Parameters + ---------- + ds : xarray.Dataset + A model dataset containing an y-coordinate and an attribute 'extent'. + + Returns + ------- + delc : np.ndarray + The cell-size along columns (of length nrow). + + """ + assert ds.gridtype == "structured" + y = (ds.extent[3] - ds.y).values + delc = _get_delta_along_axis(y) + return delc + + +def _get_delta_along_axis(x): + """Internal method to determine delr or delc from x or y relative to xmin or ymax""" + delr = [x[0] * 2] + for xi in x[1:]: + delr.append((xi - np.sum(delr)) * 2) + return np.array(delr) + + def ds_to_structured_grid( ds_in, extent, @@ -163,17 +215,11 @@ def ds_to_structured_grid( x=x, y=y, method=method, kwargs={"fill_value": "extrapolate"} ) ds_out.attrs = attrs - # ds_out = ds_out.expand_dims({"ncol": len(x), "nrow": len(y)}) - ds_out["delr"] = ("ncol",), delr - ds_out["delc"] = ("nrow",), delc return ds_out ds_out = xr.Dataset(coords={"y": y, "x": x, "layer": ds_in.layer.data}, attrs=attrs) for var in ds_in.data_vars: ds_out[var] = structured_da_to_ds(ds_in[var], ds_out, method=method) - # ds_out = ds_out.expand_dims({"ncol": len(x), "nrow": len(y)}) - ds_out["delr"] = ("x",), delr - ds_out["delc"] = ("y",), delc return ds_out @@ -658,9 +704,13 @@ def get_affine(ds, sx=None, sy=None): """Get the affine-transformation, from pixel to real-world coordinates.""" attrs = _get_attrs(ds) if sx is None: - sx = attrs["delr"] + sx = get_delr(ds) + assert len(np.unique(sx)) == 1, "Affine-transformation needs a constant delr" + sx = sx[0] if sy is None: - sy = -attrs["delc"] + sy = get_delc(ds) + assert len(np.unique(sy)) == 1, "Affine-transformation needs a constant delc" + sy = sy[0] if "angrot" in attrs: xorigin = attrs["xorigin"] diff --git a/nlmod/gis.py b/nlmod/gis.py index 3998791f..17d4709c 100644 --- a/nlmod/gis.py +++ b/nlmod/gis.py @@ -143,9 +143,6 @@ def struc_da_to_gdf(model_ds, data_variables, polygons=None, dealing_with_time=" raise ValueError( f"expected dimensions ('layer', 'y', 'x'), got {da.dims}" ) - # TODO: remove when delr/delc are removed as data vars - elif da_name in ["delr", "delc"]: - continue else: raise NotImplementedError( f"expected two or three dimensions got {no_dims} for data variable {da_name}" @@ -289,9 +286,7 @@ def ds_to_vector_file( if model_ds.gridtype == "structured": gdf = struc_da_to_gdf(model_ds, (da_name,), polygons=polygons) elif model_ds.gridtype == "vertex": - # TODO: remove when delr/dec are removed - if da_name not in ["delr", "delc"]: - gdf = vertex_da_to_gdf(model_ds, (da_name,), polygons=polygons) + gdf = vertex_da_to_gdf(model_ds, (da_name,), polygons=polygons) if driver == "GPKG": gdf.to_file(fname_gpkg, layer=da_name, driver=driver) else: diff --git a/nlmod/gwf/gwf.py b/nlmod/gwf/gwf.py index 393bb4a7..ce45e29c 100644 --- a/nlmod/gwf/gwf.py +++ b/nlmod/gwf/gwf.py @@ -8,10 +8,10 @@ import warnings import flopy -import numpy as np import xarray as xr from ..dims import grid +from ..dims.resample import get_delr, get_delc from ..dims.layers import get_idomain from ..sim import ims, sim, tdis from ..util import _get_value_from_ds_attr, _get_value_from_ds_datavar @@ -109,11 +109,6 @@ def _dis(ds, model, length_units="METERS", pname="dis", **kwargs): return disv(ds, model, length_units=length_units) # check attributes - for att in ["delr", "delc"]: - if att in ds.attrs: - if isinstance(ds.attrs[att], np.float32): - ds.attrs[att] = float(ds.attrs[att]) - if "angrot" in ds.attrs and ds.attrs["angrot"] != 0.0: xorigin = ds.attrs["xorigin"] yorigin = ds.attrs["yorigin"] @@ -135,8 +130,8 @@ def _dis(ds, model, length_units="METERS", pname="dis", **kwargs): nlay=ds.sizes["layer"], nrow=ds.sizes["y"], ncol=ds.sizes["x"], - delr=ds["delr"].values if "delr" in ds else ds.delr, - delc=ds["delc"].values if "delc" in ds else ds.delc, + delr=get_delr(ds), + delc=get_delc(ds), top=ds["top"].data, botm=ds["botm"].data, idomain=idomain, @@ -154,8 +149,8 @@ def _dis(ds, model, length_units="METERS", pname="dis", **kwargs): nlay=ds.sizes["layer"], nrow=ds.sizes["y"], ncol=ds.sizes["x"], - delr=ds["delr"].values if "delr" in ds else ds.delr, - delc=ds["delc"].values if "delc" in ds else ds.delc, + delr=get_delr(ds), + delc=get_delc(ds), top=ds["top"].data, botm=ds["botm"].data, idomain=idomain, diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index 07d7c60c..1cf66719 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -12,7 +12,7 @@ from ..dims.grid import gdf_to_grid from ..dims.layers import get_idomain -from ..dims.resample import get_extent_polygon, extent_to_polygon +from ..dims.resample import get_extent_polygon, extent_to_polygon, get_delr, get_delc from ..read import bgt, waterboard from ..cache import cache_pickle @@ -147,7 +147,11 @@ def agg_de_lange(group, cid, ds, c1=0.0, c0=1.0, N=1e-3, crad_positive=True): # correction if group contains multiple shapes # but covers whole cell if group.area.sum() == A: - li = A / np.max([ds.delr, ds.delc]) + delr = get_delr(ds) + assert len(np.unique(delr)) == 1, "Variable grid size is not yet supported" + delc = get_delc(ds) + assert len(np.unique(delc)) == 1, "Variable grid size is not yet supported" + li = A / np.max([delr[0], delc[0]]) # width B = group.area.sum(skipna=True) / li diff --git a/nlmod/read/rws.py b/nlmod/read/rws.py index 4b00e3c3..bc2f36e3 100644 --- a/nlmod/read/rws.py +++ b/nlmod/read/rws.py @@ -37,8 +37,7 @@ def get_gdf_surface_water(ds): return gdf_swater -# TODO: temporary fix until delr/delc are removed completely -@cache.cache_netcdf(coords_3d=True, datavars=["delr", "delc"]) +@cache.cache_netcdf(coords_3d=True) def get_surface_water(ds, da_basename): """create 3 data-arrays from the shapefile with surface water: @@ -92,8 +91,7 @@ def get_surface_water(ds, da_basename): return ds_out -# TODO: temporary fix until delr/delc are removed completely -@cache.cache_netcdf(coords_2d=True, datavars=["delc", "delr"]) +@cache.cache_netcdf(coords_2d=True) def get_northsea(ds, da_name="northsea"): """Get Dataset which is 1 at the northsea and 0 everywhere else. Sea is defined by rws surface water shapefile. From e76ba79d17b16e0f13978939a2dd2e73098dc19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Mon, 17 Jun 2024 11:46:03 +0200 Subject: [PATCH 47/85] Update regis.py --- nlmod/read/regis.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nlmod/read/regis.py b/nlmod/read/regis.py index 90787dcc..7dd83e23 100644 --- a/nlmod/read/regis.py +++ b/nlmod/read/regis.py @@ -12,8 +12,7 @@ logger = logging.getLogger(__name__) -REGIS_URL = "http://www.dinodata.nl:80/opendap/REGIS/REGIS.nc" -# REGIS_URL = 'https://www.dinodata.nl/opendap/hyrax/REGIS/REGIS.nc' +REGIS_URL = "https://dinodata.nl/opendap/REGIS/REGIS.nc" @cache.cache_netcdf() From 4207435c9b90b529688a65c7cbef31337d8161d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Mon, 17 Jun 2024 12:09:15 +0200 Subject: [PATCH 48/85] another try... --- nlmod/read/regis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlmod/read/regis.py b/nlmod/read/regis.py index 7dd83e23..0109f312 100644 --- a/nlmod/read/regis.py +++ b/nlmod/read/regis.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) -REGIS_URL = "https://dinodata.nl/opendap/REGIS/REGIS.nc" +REGIS_URL = "http://dinodata.nl/opendap/REGIS/REGIS.nc" @cache.cache_netcdf() From 704b92e6b343ef291c9009467900139afd53fc64 Mon Sep 17 00:00:00 2001 From: OnnoEbbens Date: Mon, 17 Jun 2024 20:56:19 +0200 Subject: [PATCH 49/85] use https url for REGIS --- nlmod/read/regis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nlmod/read/regis.py b/nlmod/read/regis.py index 0109f312..01260c88 100644 --- a/nlmod/read/regis.py +++ b/nlmod/read/regis.py @@ -12,8 +12,8 @@ logger = logging.getLogger(__name__) -REGIS_URL = "http://dinodata.nl/opendap/REGIS/REGIS.nc" - +#REGIS_URL = "http://www.dinodata.nl:80/opendap/REGIS/REGIS.nc" +REGIS_URL = "https://www.dinodata.nl/opendap/hyrax/REGIS/REGIS.nc" @cache.cache_netcdf() def get_combined_layer_models( From d9515cd5774079343d2f1765f8d50ad3a9e841e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Thu, 20 Jun 2024 09:59:17 +0200 Subject: [PATCH 50/85] Remove warning from nlmod.gwf.output.get_flow_lower_face --- nlmod/gwf/output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlmod/gwf/output.py b/nlmod/gwf/output.py index 422f1db3..f0d0f356 100644 --- a/nlmod/gwf/output.py +++ b/nlmod/gwf/output.py @@ -364,7 +364,7 @@ def get_flow_lower_face( mask = grb_ja[ia] >= ja_start_next_layer if mask.any(): # assert mask.sum() == 1 - flf_index[ilay, icell2d] = int(ia[mask]) + flf_index[ilay, icell2d] = int(ia[mask][0]) coords = ds["botm"][lays].coords else: coords = ds["botm"].coords From 0e424d9f50ba44cbf8fa189a7fa49db3c476a962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Thu, 20 Jun 2024 10:56:02 +0200 Subject: [PATCH 51/85] Set netCDF4 version lower than 1.7.0 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 67f499d2..e01548c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ grib = ["cfgrib", "ecmwflibs"] test = ["pytest>=7", "pytest-cov", "pytest-dependency"] nbtest = ["nbformat", "nbconvert>6.4.5"] lint = ["flake8", "isort", "black[jupyter]"] -ci = ["nlmod[full,lint,test,nbtest]", "netCDF4>=1.6.3", "pandas<2.1.0"] +ci = ["nlmod[full,lint,test,nbtest]", "netCDF4<1.7.0", "pandas<2.1.0"] rtd = [ "nlmod[full]", "ipython", @@ -78,7 +78,7 @@ rtd = [ "nbsphinx", "sphinx_rtd_theme==1.0.0", "nbconvert==7.13.0", - "netCDF4>=1.6.3", + "netCDF4<1.7.0", ] [tool.setuptools.dynamic] From cdb84dc4a100a918d2b157af5b9db4d4179307cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Thu, 20 Jun 2024 11:25:19 +0200 Subject: [PATCH 52/85] Simplify regis and geotop urls Links from https://www.dinoloket.nl/modelbestanden-aanvragen/netcdf --- nlmod/read/geotop.py | 4 ++-- nlmod/read/regis.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nlmod/read/geotop.py b/nlmod/read/geotop.py index b610f005..536172c1 100644 --- a/nlmod/read/geotop.py +++ b/nlmod/read/geotop.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -GEOTOP_URL = r"http://www.dinodata.nl/opendap/GeoTOP/geotop.nc" +GEOTOP_URL = "https://dinodata.nl/opendap/GeoTOP/geotop.nc" def get_lithok_props(rgb_colors=True): @@ -591,7 +591,7 @@ def aggregate_to_ds( if "layer" in top.dims: top = top[0].drop_vars("layer") else: - if "layer" in ds["top"].dims: + if "layer" in ds["top"].dims: top = ds["top"][ilay].drop_vars("layer") else: top = ds["botm"][ilay - 1].drop_vars("layer") diff --git a/nlmod/read/regis.py b/nlmod/read/regis.py index 01260c88..7dd83e23 100644 --- a/nlmod/read/regis.py +++ b/nlmod/read/regis.py @@ -12,8 +12,8 @@ logger = logging.getLogger(__name__) -#REGIS_URL = "http://www.dinodata.nl:80/opendap/REGIS/REGIS.nc" -REGIS_URL = "https://www.dinodata.nl/opendap/hyrax/REGIS/REGIS.nc" +REGIS_URL = "https://dinodata.nl/opendap/REGIS/REGIS.nc" + @cache.cache_netcdf() def get_combined_layer_models( From ce0fdf33c79034f6da9ebf4dbc58af92f6d2489a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Thu, 27 Jun 2024 11:23:43 +0200 Subject: [PATCH 53/85] add check for time coord, if not default to None - allow reading heads da using ds that has no time discretization. --- nlmod/mfoutput/mfoutput.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nlmod/mfoutput/mfoutput.py b/nlmod/mfoutput/mfoutput.py index 14870372..f7b21ed5 100644 --- a/nlmod/mfoutput/mfoutput.py +++ b/nlmod/mfoutput/mfoutput.py @@ -71,8 +71,8 @@ def _get_time_index(fobj, ds=None, gwf_or_gwt=None): elif ds is not None: tindex = ds_time_idx( fobj.get_times(), - start_datetime=ds.time.attrs["start"], - time_units=ds.time.attrs["time_units"], + start_datetime=(ds.time.attrs["start"] if "time" in ds else None), + time_units=(ds.time.attrs["time_units"] if "time" in ds else None), ) return tindex From f70f1005805b943b74c1856c441e4e64359a9aec Mon Sep 17 00:00:00 2001 From: Bas des Tombe Date: Thu, 27 Jun 2024 11:26:37 +0200 Subject: [PATCH 54/85] Patch auto-download executables (#354) * Patch get_bin_directory() * Removed trailing whitespaces * Remove explicit download_mfbinaries in favor of auto download * Write metadata in case metadata is not written * Add warning * Please codacy * Please codacy with adding encoding to writing metadatafile * No warning when running tests as it is expected behavior --- .github/workflows/ci.yml | 5 --- README.md | 9 +----- docs/examples/00_model_from_scratch.ipynb | 19 ------------ docs/getting_started.rst | 6 ++-- nlmod/util.py | 38 ++++++++++++++++++++--- 5 files changed, 36 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60c27dae..9c2b3fdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,11 +39,6 @@ jobs: # exit-zero treats all errors as warnings. flake8 . --count --exit-zero --max-complexity=10 --max-line-length=80 --statistics - - name: Download executables needed for tests - shell: bash -l {0} - run: | - python -c "import nlmod; nlmod.util.download_mfbinaries()" - - name: Run tests only env: NHI_GWO_USERNAME: ${{ secrets.NHI_GWO_USERNAME}} diff --git a/README.md b/README.md index 1bfec90f..0286b4d9 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,4 @@ notoriously hard to install on certain platforms. Please see the ## Getting started -If you are using `nlmod` for the first time you need to download the MODFLOW -executables. You can easily download these executables by running this Python code: - - import nlmod - nlmod.download_mfbinaries() - -After you've downloaded the executables you can run the Jupyter Notebooks in the -examples folder. These notebooks illustrate how to use the `nlmod` package. +Start with the Jupyter Notebooks in the examples folder. These notebooks illustrate how to use the `nlmod` package. diff --git a/docs/examples/00_model_from_scratch.ipynb b/docs/examples/00_model_from_scratch.ipynb index 40579e4f..3163a444 100644 --- a/docs/examples/00_model_from_scratch.ipynb +++ b/docs/examples/00_model_from_scratch.ipynb @@ -35,25 +35,6 @@ "nlmod.util.get_color_logger(\"INFO\");" ] }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Download MODFLOW-binaries\n", - "To run MODFLOW, we need to download the MODFLOW-excecutables. We do this with the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if not nlmod.util.check_presence_mfbinaries():\n", - " nlmod.download_mfbinaries()" - ] - }, { "attachments": {}, "cell_type": "markdown", diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 4d995c5c..378af872 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -72,10 +72,8 @@ MODFLOW 6 model given a model dataset:: # ... add some boundary condition packages (GHB, RIV, DRN, ...) -Running the model requires the modflow binaries provided by the USGS. Those can -be downloaded with:: - - nlmod.download_mfbinaries() +The MODFLOW 6 executable is automatically downloaded and installed to your system +when building the first model. Writing and running the model can then be done using:: diff --git a/nlmod/util.py b/nlmod/util.py index 16c65a6d..0ea38fa8 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -201,7 +201,7 @@ def get_bin_directory( Parameters ---------- exe_name : str, optional - The name of the executable, by default None. + The name of the executable, by default mf6. bindir : Path, optional The directory where the executables are stored, by default "mf6". download_if_not_found : bool, optional @@ -211,12 +211,13 @@ def get_bin_directory( "modflow6", or "modflow6-nightly-build". If repo and version_tag are provided the most recent installation location of MODFLOW is found in flopy metadata that respects `version_tag` and `repo`. If not found, the executables are downloaded - using repo and version_tag. + using repo and version_tag. repo cannot be None. version_tag : str, default None GitHub release ID: for example "18.0" or "latest". If repo and version_tag are provided the most recent installation location of MODFLOW is found in flopy metadata that respects `version_tag` and `repo`. If not found, the executables are downloaded - using repo and version_tag. + using repo and version_tag. If version_tag is None, no version check is performed + on present executables and if no exe is found, the latest version is downloaded. Returns ------- @@ -233,7 +234,7 @@ def get_bin_directory( if sys.platform.startswith("win") and not exe_name.endswith(".exe"): exe_name += ".exe" - enable_version_check = version_tag is not None and repo is not None + enable_version_check = version_tag is not None # If exe_name is a full path if Path(exe_name).exists(): @@ -283,7 +284,11 @@ def get_bin_directory( # Else download the executables if download_if_not_found: - download_mfbinaries(bindir=bindir, version_tag=version_tag, repo=repo) + download_mfbinaries( + bindir=bindir, + version_tag=version_tag if version_tag is not None else "latest", + repo=repo + ) # Rerun this function return get_bin_directory( @@ -404,6 +409,29 @@ def download_mfbinaries(bindir=None, version_tag="latest", repo="executables"): get_modflow(bindir=str(bindir), release_id=version_tag, repo=repo) + # Ensure metadata is saved. + # https://github.com/modflowpy/flopy/blob/ + # 0748dcb9e4641b5ad9616af115dd3be906f98f50/flopy/utils/get_modflow.py#L623 + flopy_metadata_fp = flopy_appdata_path / "get_modflow.json" + + if not flopy_metadata_fp.exists(): + if "pytest" not in str(bindir) and "pytest" not in sys.modules: + logger.warning( + f"flopy metadata file not found at {flopy_metadata_fp}. " + "After downloading and installing the executables. " + "Creating a new metadata file." + ) + + release_metadata = get_release(tag=version_tag, repo=repo, quiet=True) + install_metadata = { + "release_id": release_metadata["tag_name"], + "repo": repo, + "bindir": str(bindir), + } + + with open(flopy_metadata_fp, "w", encoding="UTF-8") as f: + json.dump([install_metadata], f, indent=4) + # download the provisional version of modpath from Github download_modpath_provisional_exe(bindir=bindir, timeout=120) From 60ead3d4fcedc2efa19268638ca8b803173971c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Thu, 27 Jun 2024 13:39:39 +0200 Subject: [PATCH 55/85] add modelextent plot --- nlmod/plot/plot.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index 7ff8d0fe..2134d556 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -11,6 +11,8 @@ from matplotlib.colors import ListedColormap, Normalize from matplotlib.patches import Patch from mpl_toolkits.axes_grid1 import make_axes_locatable +from geopandas import GeoDataFrame +from shapely.geometry import Polygon from ..dims.grid import modelgrid_from_ds from ..dims.resample import get_affine_mod_to_world, get_extent @@ -48,6 +50,47 @@ def modelgrid(ds, ax=None, **kwargs): return ax +def modelextent(ds, ax=None, **kwargs): + """Plot model extent. + + Parameters + ---------- + ds : xarray.Dataset + The dataset containing the data. + ax : matplotlib.axes.Axes, optional + The axes object to plot on. If not provided, a new figure and axes will be + created. + **kwargs + Additional keyword arguments to pass to the boundary plot. + + Returns + ------- + ax : matplotlib.axes.Axes + axes object + """ + extent = xmin, xmax, ymin, ymax = get_extent(ds, rotated=True) + dx = 0.05 * (xmax - xmin) + dy = 0.05 * (ymax - ymin) + if ax is None: + _, ax = plt.subplots(figsize=(10, 10)) + ax.axis("scaled") + + ax.axis([xmin - dx, xmax + dx, ymin - dy, ymax + dy]) + xy = [ + (xmin, ymin), + (xmax, ymin), + (xmax, ymax), + (xmin, ymax), + (xmin, ymin), + ] + gdf = GeoDataFrame(geometry=[Polygon(xy)]) + extent = None if ax.get_autoscale_on() else ax.axis() + gdf.boundary.plot(ax=ax, **kwargs) + if extent is not None: + ax.axis(extent) + return ax + + def facet_plot( gwf, ds, From ca1ef9f155a806d7ff9a0f497c900ea1786b36a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Mon, 1 Jul 2024 11:29:21 +0200 Subject: [PATCH 56/85] Fix 02_surface_water.ipynb --- docs/examples/02_surface_water.ipynb | 10 +--------- docs/examples/03_local_grid_refinement.ipynb | 16 +--------------- docs/examples/06_gridding_vector_data.ipynb | 8 +------- docs/examples/09_schoonhoven.ipynb | 16 +--------------- 4 files changed, 4 insertions(+), 46 deletions(-) diff --git a/docs/examples/02_surface_water.ipynb b/docs/examples/02_surface_water.ipynb index c0a7033b..1dcbba79 100644 --- a/docs/examples/02_surface_water.ipynb +++ b/docs/examples/02_surface_water.ipynb @@ -490,7 +490,7 @@ "xlim = ax.get_xlim()\n", "ylim = ax.get_ylim()\n", "gwf.modelgrid.plot(ax=ax)\n", - "ax.set_xlim(xlim[0], xlim[0] + ds.delr[-1] * 1.1)\n", + "ax.set_xlim(xlim[0], xlim[0] + nlmod.grid.get_delr(ds)[-1] * 1.1)\n", "ax.set_ylim(ylim)\n", "ax.set_title(f\"Surface water shapes in cell: {cid}\")" ] @@ -788,14 +788,6 @@ "cbar = fig.colorbar(qm, shrink=1.0)\n", "cbar.set_label(\"head [m NAP]\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1cbc42bf", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/examples/03_local_grid_refinement.ipynb b/docs/examples/03_local_grid_refinement.ipynb index f2326d50..8ae34a60 100644 --- a/docs/examples/03_local_grid_refinement.ipynb +++ b/docs/examples/03_local_grid_refinement.ipynb @@ -442,22 +442,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "nlmod", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.4" + "name": "python" } }, "nbformat": 4, diff --git a/docs/examples/06_gridding_vector_data.ipynb b/docs/examples/06_gridding_vector_data.ipynb index 8462a620..11267282 100644 --- a/docs/examples/06_gridding_vector_data.ipynb +++ b/docs/examples/06_gridding_vector_data.ipynb @@ -556,14 +556,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "nlmod", - "language": "python", - "name": "python3" - }, "language_info": { - "name": "python", - "version": "3.9.4" + "name": "python" } }, "nbformat": 4, diff --git a/docs/examples/09_schoonhoven.ipynb b/docs/examples/09_schoonhoven.ipynb index e107c82d..1444756c 100644 --- a/docs/examples/09_schoonhoven.ipynb +++ b/docs/examples/09_schoonhoven.ipynb @@ -827,22 +827,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "nlmod", - "language": "python", - "name": "python3" - }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.4" + "name": "python" } }, "nbformat": 4, From e808140d6476684a06888f09b6a58f758a1b5a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Mon, 1 Jul 2024 15:00:48 +0200 Subject: [PATCH 57/85] Minor changes to resample and regis --- nlmod/dims/resample.py | 2 ++ nlmod/read/regis.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index f3ff7fe1..01725ca7 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -195,6 +195,8 @@ def ds_to_structured_grid( """ assert isinstance(ds_in, xr.core.dataset.Dataset) + if hasattr(ds_in, "gridtype"): + assert ds_in.attrs["gridtype"] == "structured" if delc is None: delc = delr diff --git a/nlmod/read/regis.py b/nlmod/read/regis.py index 7dd83e23..1f27c6c7 100644 --- a/nlmod/read/regis.py +++ b/nlmod/read/regis.py @@ -195,7 +195,13 @@ def get_regis( def add_geotop_to_regis_layers( - rg, gt, layers="HLc", geotop_k=None, remove_nan_layers=True, anisotropy=1.0 + rg, + gt, + layers="HLc", + geotop_k=None, + remove_nan_layers=True, + anisotropy=1.0, + gt_layered=None, ): """Combine geotop and regis in such a way that the one or more layers in Regis are replaced by the geo_eenheden of geotop. @@ -216,6 +222,10 @@ def add_geotop_to_regis_layers( anisotropy : float, optional The anisotropy value (kh/kv) used when there are no kv values in df. The default is 1.0. + gt_layered : xarray.Dataset + A layered representation of the geotop-dataset. By supplying this parameter, the + user can change the GeoTOP-layering, which is usueally defined by + nlmod.read.geotop.to_model_layers(gt). Returns ------- @@ -253,8 +263,11 @@ def add_geotop_to_regis_layers( rg["top"] = rg["botm"] + calculate_thickness(rg) for layer in layers: - # transform geotop data into layers - gtl = geotop.to_model_layers(gt) + if gt_layered is not None: + gtl = gt_layered.copy(deep=True) + else: + # transform geotop data into layers + gtl = geotop.to_model_layers(gt) # temporarily add layer dimension to top in gtl gtl["top"] = gtl["botm"] + calculate_thickness(gtl) From 5a740ef110f3e8b8ccb78a8de3fe360dbcf772f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Tue, 2 Jul 2024 10:19:59 +0200 Subject: [PATCH 58/85] Fix the animation of the cross-section in the documentation --- docs/examples/03_local_grid_refinement.ipynb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/examples/03_local_grid_refinement.ipynb b/docs/examples/03_local_grid_refinement.ipynb index 8ae34a60..2ccf8c01 100644 --- a/docs/examples/03_local_grid_refinement.ipynb +++ b/docs/examples/03_local_grid_refinement.ipynb @@ -28,6 +28,7 @@ "import geopandas as gpd\n", "import hydropandas as hpd\n", "import matplotlib.pyplot as plt\n", + "from IPython.display import HTML\n", "import nlmod\n", "import warnings" ] @@ -327,12 +328,14 @@ "x = np.mean(extent[:2])\n", "line = [(x, extent[2]), (x, extent[3])]\n", "\n", - "\n", "ds[\"heads\"] = nlmod.gwf.get_heads_da(ds)\n", "\n", "f, ax = plt.subplots(figsize=(10, 6))\n", "dcs = nlmod.plot.DatasetCrossSection(ds, line, ax=ax, zmin=-30.0, zmax=5.0)\n", "\n", + "# plot a map with the locaton of the cross-section (which is shown below the cross-section)\n", + "dcs.plot_map_cs(lw=5, figsize=10)\n", + "\n", "# add labels with layer names\n", "ax.set_xlabel(\"distance [m]\")\n", "ax.set_ylabel(\"elevation [mNAP]\")\n", @@ -340,18 +343,19 @@ "dcs.plot_grid(lw=0.25, edgecolor=\"k\", alpha=0.5, vertical=False)\n", "dcs.plot_layers(alpha=0.0, min_label_area=5e4)\n", "dcs.plot_surface(ds[\"top\"], lw=1.0, color=\"k\")\n", + "f.tight_layout(pad=0.0)\n", "\n", - "fname = os.path.join(ds.figdir, f\"anim_xsec_x{int(x)}_head.mp4\")\n", - "dcs.animate(\n", + "anim = dcs.animate(\n", " ds[\"heads\"],\n", " cmap=\"Spectral_r\",\n", " head=ds[\"heads\"],\n", " plot_title=f\"doorsnede at x={int(x)}\",\n", " date_fmt=\"%Y-%m-%d\",\n", - " fname=fname,\n", ")\n", "\n", - "dcs.plot_map_cs(lw=5)" + "# close the figure of the cross-section, so it will not be shown below the animation\n", + "plt.close(f)\n", + "HTML(anim.to_jshtml())" ] }, { From 0c295be3cd50cdd6e02b694d0277fdfe2ffe7635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Tue, 2 Jul 2024 13:47:48 +0200 Subject: [PATCH 59/85] Move methods from dims.reample to dims.grid and util --- nlmod/dims/base.py | 6 +- nlmod/dims/grid.py | 159 +++++++++++++++++++++++++++++++-- nlmod/dims/resample.py | 177 +++++++++++-------------------------- nlmod/gis.py | 3 +- nlmod/gwf/gwf.py | 2 +- nlmod/gwf/output.py | 3 +- nlmod/gwf/surface_water.py | 9 +- nlmod/mfoutput/mfoutput.py | 7 +- nlmod/plot/dcs.py | 3 +- nlmod/plot/plot.py | 40 +++++---- nlmod/plot/plotutil.py | 2 +- nlmod/read/ahn.py | 3 +- nlmod/read/bgt.py | 2 +- nlmod/read/jarkus.py | 4 +- nlmod/read/knmi.py | 2 +- nlmod/util.py | 59 ++++++++++++- 16 files changed, 306 insertions(+), 175 deletions(-) diff --git a/nlmod/dims/base.py b/nlmod/dims/base.py index c2b729b6..43c17363 100644 --- a/nlmod/dims/base.py +++ b/nlmod/dims/base.py @@ -8,7 +8,7 @@ from .. import util from ..epsg28992 import EPSG_28992 -from . import resample +from . import resample, grid from .layers import fill_nan_top_botm_kh_kv logger = logging.getLogger(__name__) @@ -364,7 +364,7 @@ def _get_structured_grid_ds( } if angrot != 0.0: - affine = resample.get_affine_mod_to_world(attrs) + affine = grid.get_affine_mod_to_world(attrs) xc, yc = affine * np.meshgrid(xcenters, ycenters) coords["xc"] = (("y", "x"), xc) coords["yc"] = (("y", "x"), yc) @@ -645,7 +645,7 @@ def get_ds( x, y = resample.get_xy_mid_structured(attrs["extent"], delr, delc) coords = {"x": x, "y": y, "layer": layer} if angrot != 0.0: - affine = resample.get_affine_mod_to_world(attrs) + affine = grid.get_affine_mod_to_world(attrs) xc, yc = affine * np.meshgrid(x, y) coords["xc"] = (("y", "x"), xc) coords["yc"] = (("y", "x"), yc) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 2aadff70..50245f61 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -22,6 +22,7 @@ from flopy.utils.gridintersect import GridIntersect from packaging import version from scipy.interpolate import griddata +from affine import Affine from shapely.affinity import affine_transform from shapely.geometry import Point, Polygon from tqdm import tqdm @@ -35,14 +36,6 @@ remove_inactive_layers, ) from .rdp import rdp -from .resample import ( - affine_transform_gdf, - get_affine_mod_to_world, - get_affine_world_to_mod, - structured_da_to_ds, - get_delr, - get_delc, -) logger = logging.getLogger(__name__) @@ -229,6 +222,58 @@ def modelgrid_from_ds(ds, rotated=True, nlay=None, top=None, botm=None, **kwargs return modelgrid +def get_delr(ds): + """ + Get the distance along rows (delr) from the x-coordinate of a structured model + dataset. + + Parameters + ---------- + ds : xarray.Dataset + A model dataset containing an x-coordinate and an attribute 'extent'. + + Returns + ------- + delr : np.ndarray + The cell-size along rows (of length ncol). + + """ + assert ds.gridtype == "structured" + x = (ds.x - ds.extent[0]).values + delr = _get_delta_along_axis(x) + return delr + + +def get_delc(ds): + """ + Get the distance along columns (delc) from the y-coordinate of a structured model + dataset. + + Parameters + ---------- + ds : xarray.Dataset + A model dataset containing an y-coordinate and an attribute 'extent'. + + Returns + ------- + delc : np.ndarray + The cell-size along columns (of length nrow). + + """ + assert ds.gridtype == "structured" + y = (ds.extent[3] - ds.y).values + delc = _get_delta_along_axis(y) + return delc + + +def _get_delta_along_axis(x): + """Internal method to determine delr or delc from x or y relative to xmin or ymax""" + delr = [x[0] * 2] + for xi in x[1:]: + delr.append((xi - np.sum(delr)) * 2) + return np.array(delr) + + def modelgrid_to_vertex_ds(mg, ds, nodata=-1): """Add information about the calculation-grid to a model dataset.""" @@ -575,6 +620,8 @@ def ds_to_gridprops(ds_in, gridprops, method="nearest", icvert_nodata=-1): ds_out.coords.update({"layer": ds_in.layer, "x": x, "y": y}) # add other variables + from .resample import structured_da_to_ds + for not_interp_var in not_interp_vars: ds_out[not_interp_var] = structured_da_to_ds( da=ds_in[not_interp_var], ds=ds_out, method=method, nodata=np.NaN @@ -683,6 +730,8 @@ def update_ds_from_layer_ds(ds, layer_ds, method="nearest", **kwargs): for var in layer_ds.data_vars: ds[var] = layer_ds[var] else: + from .resample import structured_da_to_ds + for var in layer_ds.data_vars: ds[var] = structured_da_to_ds(layer_ds[var], ds, method=method) ds = extrapolate_ds(ds) @@ -1978,3 +2027,97 @@ def polygons_from_model_ds(model_ds): polygons = [affine_transform(polygon, affine) for polygon in polygons] return polygons + + +def _get_attrs(ds): + if isinstance(ds, dict): + return ds + else: + return ds.attrs + + +def get_extent_polygon(ds, rotated=True): + """Get the model extent, as a shapely Polygon.""" + attrs = _get_attrs(ds) + polygon = util.extent_to_polygon(attrs["extent"]) + if rotated and "angrot" in ds.attrs and attrs["angrot"] != 0.0: + affine = get_affine_mod_to_world(ds) + polygon = affine_transform(polygon, affine.to_shapely()) + return polygon + + +def get_extent_gdf(ds, rotated=True, crs="EPSG:28992"): + polygon = get_extent_polygon(ds, rotated=rotated) + return gpd.GeoDataFrame(geometry=[polygon], crs=crs) + + +def affine_transform_gdf(gdf, affine): + """Apply an affine transformation to a geopandas GeoDataFrame.""" + if isinstance(affine, Affine): + affine = affine.to_shapely() + gdfm = gdf.copy() + gdfm.geometry = gdf.affine_transform(affine) + return gdfm + + +def get_extent(ds, rotated=True): + """Get the model extent, corrected for angrot if necessary.""" + attrs = _get_attrs(ds) + extent = attrs["extent"] + if rotated and "angrot" in attrs and attrs["angrot"] != 0.0: + affine = get_affine_mod_to_world(ds) + xc = np.array([extent[0], extent[1], extent[1], extent[0]]) + yc = np.array([extent[2], extent[2], extent[3], extent[3]]) + xc, yc = affine * (xc, yc) + extent = [xc.min(), xc.max(), yc.min(), yc.max()] + return extent + + +def get_affine_mod_to_world(ds): + """Get the affine-transformation from model to real-world coordinates.""" + attrs = _get_attrs(ds) + xorigin = attrs["xorigin"] + yorigin = attrs["yorigin"] + angrot = attrs["angrot"] + return Affine.translation(xorigin, yorigin) * Affine.rotation(angrot) + + +def get_affine_world_to_mod(ds): + """Get the affine-transformation from real-world to model coordinates.""" + attrs = _get_attrs(ds) + xorigin = attrs["xorigin"] + yorigin = attrs["yorigin"] + angrot = attrs["angrot"] + return Affine.rotation(-angrot) * Affine.translation(-xorigin, -yorigin) + + +def get_affine(ds, sx=None, sy=None): + """Get the affine-transformation, from pixel to real-world coordinates.""" + attrs = _get_attrs(ds) + if sx is None: + sx = get_delr(ds) + assert len(np.unique(sx)) == 1, "Affine-transformation needs a constant delr" + sx = sx[0] + if sy is None: + sy = get_delc(ds) + assert len(np.unique(sy)) == 1, "Affine-transformation needs a constant delc" + sy = sy[0] + + if "angrot" in attrs: + xorigin = attrs["xorigin"] + yorigin = attrs["yorigin"] + angrot = -attrs["angrot"] + # xorigin and yorigin represent the lower left corner, while for the transform we + # need the upper left + dy = attrs["extent"][3] - attrs["extent"][2] + xoff = xorigin + dy * np.sin(angrot * np.pi / 180) + yoff = yorigin + dy * np.cos(angrot * np.pi / 180) + return ( + Affine.translation(xoff, yoff) + * Affine.scale(sx, sy) + * Affine.rotation(angrot) + ) + else: + xoff = attrs["extent"][0] + yoff = attrs["extent"][3] + return Affine.translation(xoff, yoff) * Affine.scale(sx, sy) diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index f3ff7fe1..ff88491a 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -4,14 +4,13 @@ import numpy as np import rasterio import xarray as xr -from affine import Affine from scipy.interpolate import griddata from scipy.spatial import cKDTree -from shapely.affinity import affine_transform -from shapely.geometry import Polygon + from ..util import get_da_from_da_ds + logger = logging.getLogger(__name__) @@ -92,58 +91,6 @@ def get_xy_mid_structured(extent, delr, delc, descending_y=True): raise TypeError("unexpected type for delr and/or delc") -def get_delr(ds): - """ - Get the distance along rows (delr) from the x-coordinate of a structured model - dataset. - - Parameters - ---------- - ds : xarray.Dataset - A model dataset containing an x-coordinate and an attribute 'extent'. - - Returns - ------- - delr : np.ndarray - The cell-size along rows (of length ncol). - - """ - assert ds.gridtype == "structured" - x = (ds.x - ds.extent[0]).values - delr = _get_delta_along_axis(x) - return delr - - -def get_delc(ds): - """ - Get the distance along columns (delc) from the y-coordinate of a structured model - dataset. - - Parameters - ---------- - ds : xarray.Dataset - A model dataset containing an y-coordinate and an attribute 'extent'. - - Returns - ------- - delc : np.ndarray - The cell-size along columns (of length nrow). - - """ - assert ds.gridtype == "structured" - y = (ds.extent[3] - ds.y).values - delc = _get_delta_along_axis(y) - return delc - - -def _get_delta_along_axis(x): - """Internal method to determine delr or delc from x or y relative to xmin or ymax""" - delr = [x[0] * 2] - for xi in x[1:]: - delr.append((xi - np.sum(delr)) * 2) - return np.array(delr) - - def ds_to_structured_grid( ds_in, extent, @@ -576,6 +523,8 @@ def structured_da_to_ds(da, ds, method="average", nodata=np.NaN): raise ValueError( "No extent or delr and delc in ds. Cannot determine affine." ) + from .grid import get_affine + da_out = da.rio.reproject( dst_crs=ds.rio.crs, shape=(len(ds.y), len(ds.x)), @@ -595,6 +544,8 @@ def structured_da_to_ds(da, ds, method="average", nodata=np.NaN): dims.remove("x") dims.append("icell2d") da_out = get_da_from_da_ds(ds, dims=tuple(dims), data=nodata) + from .grid import get_affine + for area in np.unique(ds["area"]): dx = dy = np.sqrt(area) x, y = get_xy_mid_structured(ds.extent, dx, dy) @@ -635,98 +586,74 @@ def structured_da_to_ds(da, ds, method="average", nodata=np.NaN): def extent_to_polygon(extent): - """Generate a shapely Polygon from an extent ([xmin, xmax, ymin, ymax])""" - nw = (extent[0], extent[2]) - no = (extent[1], extent[2]) - zo = (extent[1], extent[3]) - zw = (extent[0], extent[3]) - return Polygon([nw, no, zo, zw]) - + logger.warning( + "nlmod.resample.extent_to_polygon is deprecated. " + "Use nlmod.util.extent_to_polygon instead" + ) + from ..util import extent_to_polygon -def _get_attrs(ds): - if isinstance(ds, dict): - return ds - else: - return ds.attrs + return extent_to_polygon(extent) def get_extent_polygon(ds, rotated=True): """Get the model extent, as a shapely Polygon.""" - attrs = _get_attrs(ds) - polygon = extent_to_polygon(attrs["extent"]) - if rotated and "angrot" in ds.attrs and attrs["angrot"] != 0.0: - affine = get_affine_mod_to_world(ds) - polygon = affine_transform(polygon, affine.to_shapely()) - return polygon + logger.warning( + "nlmod.resample.get_extent_polygon is deprecated. " + "Use nlmod.grid.get_extent_polygon instead" + ) + from .grid import get_extent_polygon + + return get_extent_polygon(ds, rotated=rotated) def affine_transform_gdf(gdf, affine): """Apply an affine transformation to a geopandas GeoDataFrame.""" - if isinstance(affine, Affine): - affine = affine.to_shapely() - gdfm = gdf.copy() - gdfm.geometry = gdf.affine_transform(affine) - return gdfm + logger.warning( + "nlmod.resample.affine_transform_gdf is deprecated. " + "Use nlmod.grid.affine_transform_gdf instead" + ) + from .grid import affine_transform_gdf + + return affine_transform_gdf(gdf, affine) def get_extent(ds, rotated=True): """Get the model extent, corrected for angrot if necessary.""" - attrs = _get_attrs(ds) - extent = attrs["extent"] - if rotated and "angrot" in attrs and attrs["angrot"] != 0.0: - affine = get_affine_mod_to_world(ds) - xc = np.array([extent[0], extent[1], extent[1], extent[0]]) - yc = np.array([extent[2], extent[2], extent[3], extent[3]]) - xc, yc = affine * (xc, yc) - extent = [xc.min(), xc.max(), yc.min(), yc.max()] - return extent + logger.warning( + "nlmod.resample.get_extent is deprecated. " "Use nlmod.grid.get_extent instead" + ) + from .grid import get_extent + + return get_extent(ds, rotated=rotated) def get_affine_mod_to_world(ds): """Get the affine-transformation from model to real-world coordinates.""" - attrs = _get_attrs(ds) - xorigin = attrs["xorigin"] - yorigin = attrs["yorigin"] - angrot = attrs["angrot"] - return Affine.translation(xorigin, yorigin) * Affine.rotation(angrot) + logger.warning( + "nlmod.resample.get_affine_mod_to_world is deprecated. " + "Use nlmod.grid.get_affine_mod_to_world instead" + ) + from .grid import get_affine_mod_to_world + + return get_affine_mod_to_world(ds) def get_affine_world_to_mod(ds): """Get the affine-transformation from real-world to model coordinates.""" - attrs = _get_attrs(ds) - xorigin = attrs["xorigin"] - yorigin = attrs["yorigin"] - angrot = attrs["angrot"] - return Affine.rotation(-angrot) * Affine.translation(-xorigin, -yorigin) + logger.warning( + "nlmod.resample.get_affine_world_to_mod is deprecated. " + "Use nlmod.grid.get_affine_world_to_mod instead" + ) + from .grid import get_affine_world_to_mod + + return get_affine_world_to_mod(ds) def get_affine(ds, sx=None, sy=None): """Get the affine-transformation, from pixel to real-world coordinates.""" - attrs = _get_attrs(ds) - if sx is None: - sx = get_delr(ds) - assert len(np.unique(sx)) == 1, "Affine-transformation needs a constant delr" - sx = sx[0] - if sy is None: - sy = get_delc(ds) - assert len(np.unique(sy)) == 1, "Affine-transformation needs a constant delc" - sy = sy[0] - - if "angrot" in attrs: - xorigin = attrs["xorigin"] - yorigin = attrs["yorigin"] - angrot = -attrs["angrot"] - # xorigin and yorigin represent the lower left corner, while for the transform we - # need the upper left - dy = attrs["extent"][3] - attrs["extent"][2] - xoff = xorigin + dy * np.sin(angrot * np.pi / 180) - yoff = yorigin + dy * np.cos(angrot * np.pi / 180) - return ( - Affine.translation(xoff, yoff) - * Affine.scale(sx, sy) - * Affine.rotation(angrot) - ) - else: - xoff = attrs["extent"][0] - yoff = attrs["extent"][3] - return Affine.translation(xoff, yoff) * Affine.scale(sx, sy) + logger.warning( + "nlmod.resample.get_affine is deprecated. " "Use nlmod.grid.get_affine instead" + ) + from .grid import get_affine + + return get_affine(ds) diff --git a/nlmod/gis.py b/nlmod/gis.py index 17d4709c..21941092 100644 --- a/nlmod/gis.py +++ b/nlmod/gis.py @@ -4,8 +4,7 @@ import geopandas as gpd import numpy as np -from .dims.grid import polygons_from_model_ds -from .dims.resample import get_affine_mod_to_world +from .dims.grid import polygons_from_model_ds, get_affine_mod_to_world from .dims.layers import calculate_thickness logger = logging.getLogger(__name__) diff --git a/nlmod/gwf/gwf.py b/nlmod/gwf/gwf.py index ce45e29c..65e17cdc 100644 --- a/nlmod/gwf/gwf.py +++ b/nlmod/gwf/gwf.py @@ -11,7 +11,7 @@ import xarray as xr from ..dims import grid -from ..dims.resample import get_delr, get_delc +from ..dims.grid import get_delr, get_delc from ..dims.layers import get_idomain from ..sim import ims, sim, tdis from ..util import _get_value_from_ds_attr, _get_value_from_ds_datavar diff --git a/nlmod/gwf/output.py b/nlmod/gwf/output.py index f0d0f356..a296cd0f 100644 --- a/nlmod/gwf/output.py +++ b/nlmod/gwf/output.py @@ -7,8 +7,7 @@ import xarray as xr from shapely.geometry import Point -from ..dims.grid import modelgrid_from_ds -from ..dims.resample import get_affine_world_to_mod +from ..dims.grid import modelgrid_from_ds, get_affine_world_to_mod from ..mfoutput.mfoutput import ( _get_budget_da, _get_heads_da, diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index c61f646e..9a3851c6 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -10,9 +10,14 @@ from shapely.strtree import STRtree from tqdm import tqdm -from ..dims.grid import gdf_to_grid +from ..util import extent_to_polygon +from ..dims.grid import ( + gdf_to_grid, + get_extent_polygon, + get_delr, + get_delc, +) from ..dims.layers import get_idomain -from ..dims.resample import get_extent_polygon, extent_to_polygon, get_delr, get_delc from ..read import bgt, waterboard from ..cache import cache_pickle diff --git a/nlmod/mfoutput/mfoutput.py b/nlmod/mfoutput/mfoutput.py index f7b21ed5..e764b718 100644 --- a/nlmod/mfoutput/mfoutput.py +++ b/nlmod/mfoutput/mfoutput.py @@ -7,8 +7,11 @@ import flopy -from ..dims.grid import get_dims_coords_from_modelgrid, modelgrid_from_ds -from ..dims.resample import get_affine_mod_to_world +from ..dims.grid import ( + get_dims_coords_from_modelgrid, + modelgrid_from_ds, + get_affine_mod_to_world, +) from ..dims.time import ds_time_idx from .binaryfile import _get_binary_budget_data, _get_binary_head_data diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index f27162b0..b89d08f9 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -15,8 +15,7 @@ from shapely.affinity import affine_transform from shapely.geometry import LineString, MultiLineString, Point, Polygon -from ..dims.grid import modelgrid_from_ds -from ..dims.resample import get_affine_world_to_mod +from ..dims.grid import modelgrid_from_ds, get_affine_world_to_mod from .plotutil import get_map diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index 2134d556..b1c4f0b2 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -11,11 +11,13 @@ from matplotlib.colors import ListedColormap, Normalize from matplotlib.patches import Patch from mpl_toolkits.axes_grid1 import make_axes_locatable -from geopandas import GeoDataFrame -from shapely.geometry import Polygon -from ..dims.grid import modelgrid_from_ds -from ..dims.resample import get_affine_mod_to_world, get_extent +from ..dims.grid import ( + modelgrid_from_ds, + get_affine_mod_to_world, + get_extent, + get_extent_gdf, +) from ..read import geotop, rws from .dcs import DatasetCrossSection from .plotutil import ( @@ -50,7 +52,7 @@ def modelgrid(ds, ax=None, **kwargs): return ax -def modelextent(ds, ax=None, **kwargs): +def modelextent(ds, dx=None, ax=None, rotated=True, **kwargs): """Plot model extent. Parameters @@ -68,22 +70,15 @@ def modelextent(ds, ax=None, **kwargs): ax : matplotlib.axes.Axes axes object """ - extent = xmin, xmax, ymin, ymax = get_extent(ds, rotated=True) - dx = 0.05 * (xmax - xmin) - dy = 0.05 * (ymax - ymin) + extent = xmin, xmax, ymin, ymax = get_extent(ds, rotated=rotated) + if dx is None: + dx = max(0.05 * (xmax - xmin), 0.05 * (ymax - ymin)) if ax is None: _, ax = plt.subplots(figsize=(10, 10)) ax.axis("scaled") - ax.axis([xmin - dx, xmax + dx, ymin - dy, ymax + dy]) - xy = [ - (xmin, ymin), - (xmax, ymin), - (xmax, ymax), - (xmin, ymax), - (xmin, ymin), - ] - gdf = GeoDataFrame(geometry=[Polygon(xy)]) + ax.axis([xmin - dx, xmax + dx, ymin - dx, ymax + dx]) + gdf = get_extent_gdf(ds, rotated=rotated) extent = None if ax.get_autoscale_on() else ax.axis() gdf.boundary.plot(ax=ax, **kwargs) if extent is not None: @@ -259,7 +254,14 @@ def data_array(da, ds=None, ax=None, rotated=False, edgecolor=None, **kwargs): def geotop_lithok_in_cross_section( - line, gt=None, ax=None, legend=True, legend_loc=None, lithok_props=None, alpha=None, **kwargs + line, + gt=None, + ax=None, + legend=True, + legend_loc=None, + lithok_props=None, + alpha=None, + **kwargs, ): """PLot the lithoclass-data of GeoTOP in a cross-section. @@ -312,7 +314,7 @@ def geotop_lithok_in_cross_section( cs = DatasetCrossSection(gt, line, layer="z", ax=ax, **kwargs) array, cmap, norm = _get_geotop_cmap_and_norm(gt["lithok"], lithok_props) cs.plot_array(array, norm=norm, cmap=cmap, alpha=alpha) - + if legend: # make a legend with dummy handles _add_geotop_lithok_legend(lithok_props, ax, lithok=gt["lithok"], loc=legend_loc) diff --git a/nlmod/plot/plotutil.py b/nlmod/plot/plotutil.py index 00c134e3..07908b31 100644 --- a/nlmod/plot/plotutil.py +++ b/nlmod/plot/plotutil.py @@ -3,7 +3,7 @@ from matplotlib.patches import Polygon from matplotlib.ticker import FuncFormatter, MultipleLocator -from ..dims.resample import get_affine_mod_to_world +from ..dims.grid import get_affine_mod_to_world from ..epsg28992 import EPSG_28992 diff --git a/nlmod/read/ahn.py b/nlmod/read/ahn.py index 1bd2fda8..0c74eba1 100644 --- a/nlmod/read/ahn.py +++ b/nlmod/read/ahn.py @@ -13,7 +13,8 @@ from tqdm import tqdm from .. import cache -from ..dims.resample import get_extent, structured_da_to_ds +from ..dims.grid import get_extent +from ..dims.resample import structured_da_to_ds from ..util import get_ds_empty from .webservices import arcrest, wcs diff --git a/nlmod/read/bgt.py b/nlmod/read/bgt.py index db7b2ef9..6ac8d2cd 100644 --- a/nlmod/read/bgt.py +++ b/nlmod/read/bgt.py @@ -10,7 +10,7 @@ import requests from shapely.geometry import LineString, MultiPolygon, Point, Polygon -from ..dims.resample import extent_to_polygon +from ..util import extent_to_polygon def get_bgt( diff --git a/nlmod/read/jarkus.py b/nlmod/read/jarkus.py index 5d962973..1f342882 100644 --- a/nlmod/read/jarkus.py +++ b/nlmod/read/jarkus.py @@ -7,6 +7,7 @@ Note: if you like jazz please check this out: https://www.northseajazz.com """ + import datetime as dt import logging import os @@ -17,7 +18,8 @@ import xarray as xr from .. import cache -from ..dims.resample import fillnan_da, get_extent, structured_da_to_ds +from ..dims.grid import get_extent +from ..dims.resample import fillnan_da, structured_da_to_ds from ..util import get_da_from_da_ds, get_ds_empty logger = logging.getLogger(__name__) diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index b142848c..ddfc317d 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -8,7 +8,7 @@ from .. import cache, util from ..dims.layers import get_first_active_layer -from ..dims.resample import get_affine_mod_to_world +from ..dims.grid import get_affine_mod_to_world logger = logging.getLogger(__name__) diff --git a/nlmod/util.py b/nlmod/util.py index 16c65a6d..e3b996ff 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -13,7 +13,7 @@ import requests import xarray as xr from colorama import Back, Fore, Style -from shapely.geometry import box +from shapely.geometry import box, Polygon logger = logging.getLogger(__name__) @@ -561,6 +561,51 @@ def compare_model_extents(extent1, extent2): raise NotImplementedError("other options are not yet implemented") +def extent_to_polygon(extent): + """Generate a shapely Polygon from an extent ([xmin, xmax, ymin, ymax]) + + + Parameters + ---------- + extent : tuple, list or array + extent (xmin, xmax, ymin, ymax). + + Returns + ------- + shapely.geometry.Polygon + polygon of the extent. + + """ + nw = (extent[0], extent[2]) + no = (extent[1], extent[2]) + zo = (extent[1], extent[3]) + zw = (extent[0], extent[3]) + return Polygon([nw, no, zo, zw]) + + +def extent_to_gdf(extent, crs="EPSG:28992"): + """Create a geodataframe with a single polygon with the extent given. + + Parameters + ---------- + extent : tuple, list or array + extent. + crs : str, optional + coördinate reference system of the extent, default is EPSG:28992 + (RD new) + + Returns + ------- + gdf_extent : geopandas.GeoDataFrame + geodataframe with extent. + """ + + geom_extent = extent_to_polygon(extent) + gdf_extent = gpd.GeoDataFrame(geometry=[geom_extent], crs=crs) + + return gdf_extent + + def polygon_from_extent(extent): """Create a shapely polygon from a given extent. @@ -574,7 +619,10 @@ def polygon_from_extent(extent): polygon_ext : shapely.geometry.polygon.Polygon polygon of the extent. """ - + logger.warning( + "nlmod.util.polygon_from_extent is deprecated. " + "Use nlmod.util.extent_to_polygon instead" + ) bbox = (extent[0], extent[2], extent[1], extent[3]) polygon_ext = box(*tuple(bbox)) @@ -597,7 +645,10 @@ def gdf_from_extent(extent, crs="EPSG:28992"): gdf_extent : GeoDataFrame geodataframe with extent. """ - + logger.warning( + "nlmod.util.gdf_from_extent is deprecated. " + "Use nlmod.util.extent_to_gdf instead" + ) geom_extent = polygon_from_extent(extent) gdf_extent = gpd.GeoDataFrame(geometry=[geom_extent], crs=crs) @@ -621,7 +672,7 @@ def gdf_within_extent(gdf, extent): dataframe with only polygon features within the extent. """ # create geodataframe from the extent - gdf_extent = gdf_from_extent(extent, crs=gdf.crs) + gdf_extent = extent_to_gdf(extent, crs=gdf.crs) # check type geom_types = gdf.geom_type.unique() From ee6ed3e1cc07b642532343067b30ca14431d3b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Tue, 2 Jul 2024 14:04:26 +0200 Subject: [PATCH 60/85] Improve CI and some code cleanup (#358) * run ruff check --fix, docformatter add ruff settings to pyproject.toml * update workflows - up actions versions - remove flake8 check * restore the position of NLMOD_DATADIR * skip failing KNMI Data Platform tests for now * committed import to wrong branch * update imports in examples * more style thingies * fix nb metadata * restore import * actually skip knmidp tests * forgot parentheses --- .github/workflows/ci.yml | 14 ++- .github/workflows/python-publish.yml | 10 +- README.md | 10 +- docs/conf.py | 3 +- docs/examples/00_model_from_scratch.ipynb | 7 +- docs/examples/01_basic_model.ipynb | 5 - docs/examples/02_surface_water.ipynb | 3 +- docs/examples/03_local_grid_refinement.ipynb | 9 +- docs/examples/04_modifying_layermodels.ipynb | 8 +- docs/examples/05_caching.ipynb | 10 +- docs/examples/06_gridding_vector_data.ipynb | 15 ++- docs/examples/07_resampling.ipynb | 24 ++--- docs/examples/08_gis.ipynb | 10 +- docs/examples/09_schoonhoven.ipynb | 11 +- docs/examples/10_modpath.ipynb | 13 +-- docs/examples/11_grid_rotation.ipynb | 7 +- docs/examples/12_layer_generation.ipynb | 5 +- docs/examples/13_plot_methods.ipynb | 7 +- docs/examples/14_stromingen_example.ipynb | 4 +- docs/examples/15_geotop.ipynb | 7 +- docs/examples/16_groundwater_transport.ipynb | 9 +- docs/examples/17_unsaturated_zone_flow.ipynb | 14 ++- docs/examples/cache_example.py | 5 +- docs/examples/generate_logo.py | 5 +- docs/examples/run_all_notebooks.py | 10 +- nlmod/__init__.py | 7 +- nlmod/cache.py | 13 ++- nlmod/dims/__init__.py | 1 + nlmod/dims/attributes_encodings.py | 21 ++-- nlmod/dims/base.py | 2 - nlmod/dims/grid.py | 88 ++++++++-------- nlmod/dims/layers.py | 76 ++++++-------- nlmod/dims/rdp.py | 17 ++-- nlmod/dims/resample.py | 21 ++-- nlmod/dims/time.py | 10 +- nlmod/epsg28992.py | 2 +- nlmod/gis.py | 4 +- nlmod/gwf/__init__.py | 1 + nlmod/gwf/gwf.py | 48 ++++----- nlmod/gwf/horizontal_flow_barrier.py | 14 +-- nlmod/gwf/output.py | 26 ++--- nlmod/gwf/recharge.py | 11 +- nlmod/gwf/surface_water.py | 30 +++--- nlmod/gwf/wells.py | 20 +--- nlmod/gwt/__init__.py | 1 + nlmod/gwt/gwt.py | 23 ++--- nlmod/gwt/output.py | 16 +-- nlmod/mfoutput/mfoutput.py | 7 +- nlmod/modpath/__init__.py | 1 + nlmod/modpath/modpath.py | 19 ++-- nlmod/plot/__init__.py | 2 + nlmod/plot/dcs.py | 16 ++- nlmod/plot/plot.py | 23 +++-- nlmod/plot/plotutil.py | 3 +- nlmod/read/__init__.py | 3 +- nlmod/read/ahn.py | 21 ++-- nlmod/read/bro.py | 14 ++- nlmod/read/geotop.py | 26 +++-- nlmod/read/jarkus.py | 10 +- nlmod/read/knmi.py | 10 +- nlmod/read/knmi_data_platform.py | 22 ++-- nlmod/read/meteobase.py | 9 +- nlmod/read/nhi.py | 27 ++--- nlmod/read/regis.py | 13 +-- nlmod/read/rws.py | 18 ++-- nlmod/read/waterboard.py | 10 +- nlmod/read/webservices.py | 10 +- nlmod/sim/__init__.py | 1 + nlmod/sim/sim.py | 15 ++- nlmod/util.py | 102 +++++++++++-------- nlmod/version.py | 17 ++-- pyproject.toml | 33 ++++-- tests/test_001_model.py | 12 +-- tests/test_002_regis_geotop.py | 1 + tests/test_003_mfpackages.py | 2 +- tests/test_005_external_data.py | 4 +- tests/test_007_run_notebooks.py | 37 +++---- tests/test_008_waterschappen.py | 1 - tests/test_009_layers.py | 2 +- tests/test_018_knmi_data_platform.py | 7 +- tests/test_019_attributes_encodings.py | 1 - tests/test_021_nhi.py | 15 +-- tests/test_022_gwt.py | 4 +- tests/test_023_hfb.py | 8 +- tests/test_025_modpath.py | 4 +- tests/test_026_grid.py | 12 ++- 86 files changed, 582 insertions(+), 637 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c2b3fdf..a2dd31f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,13 +17,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9] + python-version: [3.11] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -32,12 +32,10 @@ jobs: python -m pip install --upgrade pip pip install -e .[ci] - - name: Lint with flake8 + - name: Download executables needed for tests + shell: bash -l {0} run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=80 --statistics + python -c "import nlmod; nlmod.util.download_mfbinaries()" - name: Run tests only env: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 72f83ba7..0890d7eb 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -9,22 +9,24 @@ on: jobs: deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' + - name: Install dependencies run: | python -m pip install --upgrade pip pip install build setuptools wheel + - name: build binary wheel and a source tarball run: | python -m build --sdist --wheel --outdir dist/ + - name: Publish a Python distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: diff --git a/README.md b/README.md index 0286b4d9..5b3f23f5 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,13 @@ groundwater models, makes models more reproducible and transparent. The functions in `nlmod` have four main objectives: -1. Create and adapt the temporal and spatial discretization of a MODFLOW model using an xarray Dataset (`nlmod.dims`). -2. Download and read data from external sources, project this data on the modelgrid and add this data to an xarray Dataset (`nlmod.read`). -3. Use data in an xarray Dataset to build modflow packages for both groundwater flow and transport models using FloPy (`nlmod.sim`, `nlmod.gwf` and `nlmod.gwt` for Modflow 6 and `nlmod.modpath` for Modpath). +1. Create and adapt the temporal and spatial discretization of a MODFLOW model using an + xarray Dataset (`nlmod.dims`). +2. Download and read data from external sources, project this data on the modelgrid and + add this data to an xarray Dataset (`nlmod.read`). +3. Use data in an xarray Dataset to build modflow packages for both groundwater flow + and transport models using FloPy (`nlmod.sim`, `nlmod.gwf` and `nlmod.gwt` for + Modflow 6 and `nlmod.modpath` for Modpath). 4. Visualise modeldata in Python (`nlmod.plot`) or GIS software (`nlmod.gis`). More information can be found on the documentation-website: diff --git a/docs/conf.py b/docs/conf.py index b2798667..27df1c88 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,10 +10,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -from nlmod import __version__ import os import sys +from nlmod import __version__ + sys.path.insert(0, os.path.abspath(".")) diff --git a/docs/examples/00_model_from_scratch.ipynb b/docs/examples/00_model_from_scratch.ipynb index 3163a444..f452a9e5 100644 --- a/docs/examples/00_model_from_scratch.ipynb +++ b/docs/examples/00_model_from_scratch.ipynb @@ -20,10 +20,9 @@ "outputs": [], "source": [ "import flopy as fp\n", - "import matplotlib.pyplot as plt\n", - "import nlmod\n", - "import numpy as np\n", - "import pandas as pd" + "import pandas as pd\n", + "\n", + "import nlmod" ] }, { diff --git a/docs/examples/01_basic_model.ipynb b/docs/examples/01_basic_model.ipynb index ca18a257..ed358fb3 100644 --- a/docs/examples/01_basic_model.ipynb +++ b/docs/examples/01_basic_model.ipynb @@ -18,12 +18,7 @@ "metadata": {}, "outputs": [], "source": [ - "import logging\n", - "import os\n", "\n", - "import flopy\n", - "import geopandas as gpd\n", - "import matplotlib.pyplot as plt\n", "import nlmod" ] }, diff --git a/docs/examples/02_surface_water.ipynb b/docs/examples/02_surface_water.ipynb index 1dcbba79..ab34565f 100644 --- a/docs/examples/02_surface_water.ipynb +++ b/docs/examples/02_surface_water.ipynb @@ -25,8 +25,9 @@ "import os\n", "\n", "import flopy\n", - "import rioxarray\n", "import matplotlib.pyplot as plt\n", + "import rioxarray\n", + "\n", "import nlmod" ] }, diff --git a/docs/examples/03_local_grid_refinement.ipynb b/docs/examples/03_local_grid_refinement.ipynb index 2ccf8c01..9fed4ef6 100644 --- a/docs/examples/03_local_grid_refinement.ipynb +++ b/docs/examples/03_local_grid_refinement.ipynb @@ -22,15 +22,12 @@ "source": [ "import os\n", "\n", - "import flopy\n", - "import numpy as np\n", - "import pandas as pd\n", "import geopandas as gpd\n", - "import hydropandas as hpd\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "from IPython.display import HTML\n", - "import nlmod\n", - "import warnings" + "\n", + "import nlmod" ] }, { diff --git a/docs/examples/04_modifying_layermodels.ipynb b/docs/examples/04_modifying_layermodels.ipynb index 5a37f1e9..f7fe8687 100644 --- a/docs/examples/04_modifying_layermodels.ipynb +++ b/docs/examples/04_modifying_layermodels.ipynb @@ -20,11 +20,11 @@ "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", - "import nlmod\n", "import numpy as np\n", - "import pandas as pd\n", - "from nlmod.plot import DatasetCrossSection\n", - "from shapely.geometry import LineString" + "from shapely.geometry import LineString\n", + "\n", + "import nlmod\n", + "from nlmod.plot import DatasetCrossSection" ] }, { diff --git a/docs/examples/05_caching.ipynb b/docs/examples/05_caching.ipynb index 0c705248..f2e2de21 100644 --- a/docs/examples/05_caching.ipynb +++ b/docs/examples/05_caching.ipynb @@ -20,12 +20,10 @@ "outputs": [], "source": [ "import os\n", - "import flopy\n", - "import geopandas as gpd\n", - "import matplotlib.pyplot as plt\n", - "import nlmod\n", - "import numpy as np\n", - "import xarray as xr" + "\n", + "import xarray as xr\n", + "\n", + "import nlmod" ] }, { diff --git a/docs/examples/06_gridding_vector_data.ipynb b/docs/examples/06_gridding_vector_data.ipynb index 11267282..56104eb7 100644 --- a/docs/examples/06_gridding_vector_data.ipynb +++ b/docs/examples/06_gridding_vector_data.ipynb @@ -18,17 +18,15 @@ "metadata": {}, "outputs": [], "source": [ - "import nlmod\n", + "import geopandas as gpd\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import xarray as xr\n", - "\n", - "import matplotlib.pyplot as plt\n", - "\n", - "import geopandas as gpd\n", + "from IPython.display import display\n", "from shapely.geometry import LineString, Point\n", "from shapely.geometry import Polygon as shp_polygon\n", "\n", - "from IPython.display import display" + "import nlmod" ] }, { @@ -391,8 +389,9 @@ " ds = ds.expand_dims({\"layer\": range(3)})\n", "\n", "# create some data arrays\n", - "ds[\"da1\"] = (\"layer\", \"y\", \"x\"), np.random.randint(\n", - " 0, 10, (ds.sizes[\"layer\"], ds.sizes[\"y\"], ds.sizes[\"x\"])\n", + "ds[\"da1\"] = (\n", + " (\"layer\", \"y\", \"x\"),\n", + " np.random.randint(0, 10, (ds.sizes[\"layer\"], ds.sizes[\"y\"], ds.sizes[\"x\"])),\n", ")\n", "ds[\"da2\"] = (\"y\", \"x\"), np.random.randint(0, 10, (ds.sizes[\"y\"], ds.sizes[\"x\"]))\n", "ds[\"da3\"] = (\"y\", \"x\"), np.random.randint(0, 10, (ds.sizes[\"y\"], ds.sizes[\"x\"]))\n", diff --git a/docs/examples/07_resampling.ipynb b/docs/examples/07_resampling.ipynb index 093e046b..bb9a5ce9 100644 --- a/docs/examples/07_resampling.ipynb +++ b/docs/examples/07_resampling.ipynb @@ -18,25 +18,17 @@ "metadata": {}, "outputs": [], "source": [ - "import nlmod\n", - "from nlmod import resample\n", + "\n", + "import geopandas as gpd\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import xarray as xr\n", - "import flopy\n", - "import warnings\n", - "\n", - "\n", "from matplotlib.colors import Normalize\n", - "from matplotlib.patches import Polygon\n", - "from matplotlib.collections import PatchCollection\n", - "import matplotlib.pyplot as plt\n", - "\n", - "import geopandas as gpd\n", - "from shapely.geometry import LineString, Point\n", - "from shapely.geometry import Polygon as shp_polygon\n", "from scipy.interpolate import RectBivariateSpline\n", + "from shapely.geometry import LineString, Point\n", "\n", - "from IPython.display import display" + "import nlmod\n", + "from nlmod import resample" ] }, { @@ -100,7 +92,7 @@ "outputs": [], "source": [ "ds[\"data_nan\"] = ds[\"data\"].copy()\n", - "ds[\"data_nan\"].data[0, 1] = np.NaN\n", + "ds[\"data_nan\"].data[0, 1] = np.nan\n", "\n", "fig, ax = plt.subplots()\n", "ax.set_aspect(\"equal\")\n", @@ -144,7 +136,7 @@ "outputs": [], "source": [ "dsv[\"data_nan\"] = dsv[\"data\"].copy()\n", - "dsv[\"data_nan\"][7] = np.NaN\n", + "dsv[\"data_nan\"][7] = np.nan\n", "\n", "fig, ax = plt.subplots()\n", "ax.set_aspect(\"equal\")\n", diff --git a/docs/examples/08_gis.ipynb b/docs/examples/08_gis.ipynb index cde523a3..40c56731 100644 --- a/docs/examples/08_gis.ipynb +++ b/docs/examples/08_gis.ipynb @@ -19,14 +19,12 @@ "outputs": [], "source": [ "import os\n", + "\n", "import flopy\n", - "import geopandas as gpd\n", - "import matplotlib.pyplot as plt\n", - "import nlmod\n", - "import numpy as np\n", "import xarray as xr\n", - "from IPython.display import FileLink, FileLinks\n", - "from shapely.geometry import Polygon" + "from IPython.display import FileLink\n", + "\n", + "import nlmod" ] }, { diff --git a/docs/examples/09_schoonhoven.ipynb b/docs/examples/09_schoonhoven.ipynb index 1444756c..1a381db5 100644 --- a/docs/examples/09_schoonhoven.ipynb +++ b/docs/examples/09_schoonhoven.ipynb @@ -18,18 +18,17 @@ "outputs": [], "source": [ "import os\n", + "\n", "import flopy\n", + "import geopandas as gpd\n", "import matplotlib\n", "import matplotlib.pyplot as plt\n", - "import nlmod\n", "import numpy as np\n", - "import xarray as xr\n", "import pandas as pd\n", - "import hydropandas as hpd\n", - "import geopandas as gpd\n", - "from nlmod.plot import DatasetCrossSection\n", "from shapely.geometry import LineString, Point\n", - "import warnings" + "\n", + "import nlmod\n", + "from nlmod.plot import DatasetCrossSection" ] }, { diff --git a/docs/examples/10_modpath.ipynb b/docs/examples/10_modpath.ipynb index c70458cf..0a4e77c9 100644 --- a/docs/examples/10_modpath.ipynb +++ b/docs/examples/10_modpath.ipynb @@ -28,9 +28,10 @@ "\n", "import flopy\n", "import matplotlib.pyplot as plt\n", - "import nlmod\n", "import numpy as np\n", - "import xarray as xr" + "import xarray as xr\n", + "\n", + "import nlmod" ] }, { @@ -164,7 +165,7 @@ " marker=\"o\",\n", " color=\"red\",\n", ")\n", - "ax.set_title(f\"pathlines\")\n", + "ax.set_title(\"pathlines\")\n", "ax.legend(loc=\"upper right\")" ] }, @@ -265,7 +266,7 @@ " marker=\"o\",\n", " color=\"red\",\n", " )\n", - " ax.set_title(f\"pathlines\")\n", + " ax.set_title(\"pathlines\")\n", " ax.legend(loc=\"upper right\")\n", "\n", " if i == 1:\n", @@ -370,7 +371,7 @@ " marker=\"o\",\n", " color=\"red\",\n", " )\n", - " ax.set_title(f\"pathlines\")\n", + " ax.set_title(\"pathlines\")\n", " ax.legend(loc=\"upper right\")\n", "\n", " if i == 1:\n", @@ -477,7 +478,7 @@ " marker=\"o\",\n", " color=\"red\",\n", " )\n", - " ax.set_title(f\"pathlines\")\n", + " ax.set_title(\"pathlines\")\n", " ax.legend(loc=\"upper right\")\n", "\n", " if i == 1:\n", diff --git a/docs/examples/11_grid_rotation.ipynb b/docs/examples/11_grid_rotation.ipynb index 7d60bdc0..61f3d8d0 100644 --- a/docs/examples/11_grid_rotation.ipynb +++ b/docs/examples/11_grid_rotation.ipynb @@ -30,11 +30,10 @@ "outputs": [], "source": [ "import os\n", + "\n", "import matplotlib\n", - "import nlmod\n", - "import pandas as pd\n", - "import warnings\n", - "import flopy" + "\n", + "import nlmod" ] }, { diff --git a/docs/examples/12_layer_generation.ipynb b/docs/examples/12_layer_generation.ipynb index a3185d8b..f61f1e02 100644 --- a/docs/examples/12_layer_generation.ipynb +++ b/docs/examples/12_layer_generation.ipynb @@ -26,8 +26,9 @@ "metadata": {}, "outputs": [], "source": [ - "import nlmod\n", - "from shapely.geometry import LineString" + "from shapely.geometry import LineString\n", + "\n", + "import nlmod" ] }, { diff --git a/docs/examples/13_plot_methods.ipynb b/docs/examples/13_plot_methods.ipynb index ebc79dcc..7c027527 100644 --- a/docs/examples/13_plot_methods.ipynb +++ b/docs/examples/13_plot_methods.ipynb @@ -36,12 +36,13 @@ "outputs": [], "source": [ "import os\n", + "\n", + "import flopy\n", "import matplotlib.pyplot as plt\n", "import xarray as xr\n", - "import flopy\n", + "\n", "import nlmod\n", - "from nlmod.plot import DatasetCrossSection\n", - "import warnings" + "from nlmod.plot import DatasetCrossSection" ] }, { diff --git a/docs/examples/14_stromingen_example.ipynb b/docs/examples/14_stromingen_example.ipynb index 10da127f..562b4575 100644 --- a/docs/examples/14_stromingen_example.ipynb +++ b/docs/examples/14_stromingen_example.ipynb @@ -33,11 +33,13 @@ "outputs": [], "source": [ "import os\n", + "\n", "import flopy as fp\n", "import geopandas as gpd\n", - "import nlmod\n", "from pandas import date_range\n", "\n", + "import nlmod\n", + "\n", "nlmod.util.get_color_logger(\"INFO\")\n", "print(f\"nlmod version: {nlmod.__version__}\")" ] diff --git a/docs/examples/15_geotop.ipynb b/docs/examples/15_geotop.ipynb index 13500e10..28584803 100644 --- a/docs/examples/15_geotop.ipynb +++ b/docs/examples/15_geotop.ipynb @@ -16,10 +16,11 @@ "metadata": {}, "outputs": [], "source": [ - "from shapely.geometry import LineString\n", + "import matplotlib\n", "import matplotlib.pyplot as plt\n", - "import nlmod\n", - "import matplotlib" + "from shapely.geometry import LineString\n", + "\n", + "import nlmod" ] }, { diff --git a/docs/examples/16_groundwater_transport.ipynb b/docs/examples/16_groundwater_transport.ipynb index 34d70dc7..d72ea7f8 100644 --- a/docs/examples/16_groundwater_transport.ipynb +++ b/docs/examples/16_groundwater_transport.ipynb @@ -21,11 +21,12 @@ "outputs": [], "source": [ "# import packages\n", - "import nlmod\n", - "import pandas as pd\n", - "import xarray as xr\n", "import flopy as fp\n", "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import xarray as xr\n", + "\n", + "import nlmod\n", "\n", "# set up pretty logging and show package versions\n", "nlmod.util.get_color_logger(\"INFO\")\n", @@ -575,7 +576,7 @@ " ax.set_xlabel(\"x [m]\")\n", " ax.set_ylabel(\"elevation [m NAP]\")\n", " # convert to pandas timestamp for prettier printing\n", - " ax.set_title(f\"time = {pd.Timestamp(c.time.isel(time=time_idx).values)}\");" + " ax.set_title(f\"time = {pd.Timestamp(c.time.isel(time=time_idx).values)}\")" ] }, { diff --git a/docs/examples/17_unsaturated_zone_flow.ipynb b/docs/examples/17_unsaturated_zone_flow.ipynb index b9500d4a..76b21f03 100644 --- a/docs/examples/17_unsaturated_zone_flow.ipynb +++ b/docs/examples/17_unsaturated_zone_flow.ipynb @@ -28,9 +28,11 @@ "source": [ "# import packages\n", "import os\n", + "\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", + "\n", "import nlmod\n", "\n", "# set up pretty logging and show package versions\n", @@ -47,7 +49,11 @@ "source": [ "# Ignore warning about 1d layers, in numpy 1.26.1 and flopy 3.4.3\n", "import warnings\n", - "warnings.filterwarnings(\"ignore\", message=\"Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future.\")" + "\n", + "warnings.filterwarnings(\n", + " \"ignore\",\n", + " message=\"Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future.\",\n", + ")" ] }, { @@ -94,7 +100,9 @@ "y = np.floor(y / 100) * 100\n", "dx = dy = 100\n", "extent = [x, x + dx, y, y + dy]\n", - "regis = nlmod.read.regis.get_regis(extent, drop_layer_dim_from_top=False, cachename=\"regis.nc\", cachedir=cachedir)" + "regis = nlmod.read.regis.get_regis(\n", + " extent, drop_layer_dim_from_top=False, cachename=\"regis.nc\", cachedir=cachedir\n", + ")" ] }, { diff --git a/docs/examples/cache_example.py b/docs/examples/cache_example.py index 5d33d7b0..7ca76792 100644 --- a/docs/examples/cache_example.py +++ b/docs/examples/cache_example.py @@ -1,11 +1,12 @@ -import nlmod import numpy as np import xarray as xr +import nlmod + @nlmod.cache.cache_netcdf() def func_to_create_a_dataset(number): - """create a dataarray as an example for the caching method. + """Create a dataarray as an example for the caching method. Parameters ---------- diff --git a/docs/examples/generate_logo.py b/docs/examples/generate_logo.py index c423df20..87bbfcb1 100644 --- a/docs/examples/generate_logo.py +++ b/docs/examples/generate_logo.py @@ -4,11 +4,14 @@ @author: ruben """ + import os -import nlmod + import art_tools import matplotlib.pyplot as plt +import nlmod + filled = False n = 2 dx = 10_000 diff --git a/docs/examples/run_all_notebooks.py b/docs/examples/run_all_notebooks.py index 86f5101e..a1602a31 100644 --- a/docs/examples/run_all_notebooks.py +++ b/docs/examples/run_all_notebooks.py @@ -1,11 +1,13 @@ # %% +import os +import time from glob import glob + import nbformat -from nbconvert.preprocessors import ExecutePreprocessor -import time import numpy as np +from nbconvert.preprocessors import ExecutePreprocessor + import nlmod -import os logger = nlmod.util.get_color_logger("INFO") @@ -31,7 +33,7 @@ logger.info(f"Running {notebook} succeeded in {seconds} seconds") except Exception as exception: logger.error(f"Running notebook failed: {notebook}") - elapsed_time[notebook] = np.NaN + elapsed_time[notebook] = np.nan exceptions[notebook] = exception # save results in notebook diff --git a/nlmod/__init__.py b/nlmod/__init__.py index d3905157..5582151b 100644 --- a/nlmod/__init__.py +++ b/nlmod/__init__.py @@ -1,9 +1,4 @@ -# -*- coding: utf-8 -*- -"""Created on Thu Jan 7 12:13:44 2021. - -@author: oebbe -""" - +# ruff: noqa: F401 E402 import os NLMOD_DATADIR = os.path.join(os.path.dirname(__file__), "data") diff --git a/nlmod/cache.py b/nlmod/cache.py index 1aa3672f..0ed0d441 100644 --- a/nlmod/cache.py +++ b/nlmod/cache.py @@ -720,8 +720,17 @@ def ds_contains( datavars.append("tsmult") if attrs_ds: - # set by `nlmod.base.to_model_ds()` and `nlmod.base.set_ds_attrs()`, excluding "created_on" - attrs_ds_required = ["model_name", "mfversion", "exe_name", "model_ws", "figdir", "cachedir", "transport"] + # set by `nlmod.base.to_model_ds()` and `nlmod.base.set_ds_attrs()`, + # excluding "created_on" + attrs_ds_required = [ + "model_name", + "mfversion", + "exe_name", + "model_ws", + "figdir", + "cachedir", + "transport", + ] attrs.extend(attrs_ds_required) # User-friendly error messages if missing from ds diff --git a/nlmod/dims/__init__.py b/nlmod/dims/__init__.py index d5892790..18c147c6 100644 --- a/nlmod/dims/__init__.py +++ b/nlmod/dims/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 F403 from . import base, grid, layers, resample, time from .attributes_encodings import * from .base import * diff --git a/nlmod/dims/attributes_encodings.py b/nlmod/dims/attributes_encodings.py index b93587fc..5b4e30b0 100644 --- a/nlmod/dims/attributes_encodings.py +++ b/nlmod/dims/attributes_encodings.py @@ -228,14 +228,13 @@ def get_encodings( def compute_scale_and_offset(min_value, max_value): - """ - Reduce precision of the dataset by storing it as int16 and thereby reducing the precision. - - Computes the scale_factor and offset for the dataset using a min_value and max_value to - transform the range of the dataset to the range of valid int16 values. The packed value - is computed as: + """Reduce precision of the dataset by storing it as int16. + + Computes the scale_factor and offset for the dataset using a min_value and max_value + to transform the range of the dataset to the range of valid int16 values. The packed + value is computed as: packed_value = (unpacked_value - add_offset) / scale_factor - + Parameters ---------- min_value : float @@ -259,8 +258,9 @@ def compute_scale_and_offset(min_value, max_value): def is_int16_allowed(vmin, vmax, dval_max): - """compute the loss of resolution by storing a float as int16 (`dval`). And - compare it with the maximum allowed loss of resolution (`dval_max`). + """Compute the loss of resolution by storing a float as int16 (`dval`). + + Compare it with the maximum allowed loss of resolution (`dval_max`). Parameters ---------- @@ -274,7 +274,8 @@ def is_int16_allowed(vmin, vmax, dval_max): Returns ------- bool - True if the loss of resolution is allowed, False otherwise""" + True if the loss of resolution is allowed, False otherwise + """ nsteps = 32766 + 32767 dval = (vmax - vmin) / nsteps return dval <= dval_max diff --git a/nlmod/dims/base.py b/nlmod/dims/base.py index c2b729b6..a56abe85 100644 --- a/nlmod/dims/base.py +++ b/nlmod/dims/base.py @@ -44,7 +44,6 @@ def set_ds_attrs( ds : xarray dataset model dataset. """ - if model_name is not None and len(model_name) > 16 and mfversion == "mf6": raise ValueError("model_name can not have more than 16 characters") ds.attrs["model_name"] = model_name @@ -338,7 +337,6 @@ def _get_structured_grid_ds( dictionary, and a coordinate reference system specified by `crs`, if provided. """ - if attrs is None: attrs = {} attrs.update({"gridtype": "structured"}) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 2aadff70..332c6cbc 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -39,19 +39,17 @@ affine_transform_gdf, get_affine_mod_to_world, get_affine_world_to_mod, - structured_da_to_ds, - get_delr, get_delc, + get_delr, + structured_da_to_ds, ) logger = logging.getLogger(__name__) def snap_extent(extent, delr, delc): - """ - snap the extent in such a way that an integer number of columns and rows fit - in the extent. The new extent is always equal to, or bigger than the - original extent. + """Snap the extent in such a way that an integer number of columns and rows fit in + the extent. The new extent is always equal to, or bigger than the original extent. Parameters ---------- @@ -96,7 +94,7 @@ def snap_extent(extent, delr, delc): def xy_to_icell2d(xy, ds): - """get the icell2d value of a point defined by its x and y coordinates. + """Get the icell2d value of a point defined by its x and y coordinates. Parameters ---------- @@ -118,7 +116,7 @@ def xy_to_icell2d(xy, ds): def xy_to_row_col(xy, ds): - """get the row and column values of a point defined by its x and y coordinates. + """Get the row and column values of a point defined by its x and y coordinates. Parameters ---------- @@ -142,7 +140,7 @@ def xy_to_row_col(xy, ds): def xyz_to_cid(xyz, ds=None, modelgrid=None): - """get the icell2d value of a point defined by its x and y coordinates. + """Get the icell2d value of a point defined by its x and y coordinates. Parameters ---------- @@ -231,7 +229,6 @@ def modelgrid_from_ds(ds, rotated=True, nlay=None, top=None, botm=None, **kwargs def modelgrid_to_vertex_ds(mg, ds, nodata=-1): """Add information about the calculation-grid to a model dataset.""" - warnings.warn( "'modelgrid_to_vertex_ds' is deprecated and will be removed in a" "future version, please use 'modelgrid_to_ds' instead", @@ -345,8 +342,9 @@ def get_dims_coords_from_modelgrid(mg): def gridprops_to_vertex_ds(gridprops, ds, nodata=-1): - """Gridprops is a dictionary containing keyword arguments needed to - generate a flopy modelgrid instance.""" + """Gridprops is a dictionary containing keyword arguments needed to generate a flopy + modelgrid instance. + """ _, xv, yv = zip(*gridprops["vertices"]) ds["xv"] = ("iv", np.array(xv)) ds["yv"] = ("iv", np.array(yv)) @@ -520,8 +518,8 @@ def refine( def ds_to_gridprops(ds_in, gridprops, method="nearest", icvert_nodata=-1): - """resample a dataset (xarray) on an structured grid to a new dataset with - a vertex grid. + """Resample a dataset (xarray) on an structured grid to a new dataset with a vertex + grid. Returns a dataset with resampled variables and the untouched variables. @@ -577,7 +575,7 @@ def ds_to_gridprops(ds_in, gridprops, method="nearest", icvert_nodata=-1): # add other variables for not_interp_var in not_interp_vars: ds_out[not_interp_var] = structured_da_to_ds( - da=ds_in[not_interp_var], ds=ds_out, method=method, nodata=np.NaN + da=ds_in[not_interp_var], ds=ds_out, method=method, nodata=np.nan ) has_rotation = "angrot" in ds_out.attrs and ds_out.attrs["angrot"] != 0.0 if has_rotation: @@ -601,8 +599,7 @@ def ds_to_gridprops(ds_in, gridprops, method="nearest", icvert_nodata=-1): def get_xyi_icell2d(gridprops=None, ds=None): - """Get x and y coordinates of the cell mids from the cellids in the grid - properties. + """Get x and y coordinates of the cell mids from the cellids in the grid properties. Parameters ---------- @@ -634,9 +631,9 @@ def get_xyi_icell2d(gridprops=None, ds=None): def update_ds_from_layer_ds(ds, layer_ds, method="nearest", **kwargs): - """Add variables from a layer Dataset to a model Dataset. Keep de grid- - information from the model Dataset (x and y or icell2d), but update the - layer dimension when neccesary. + """Add variables from a layer Dataset to a model Dataset. Keep de grid- information + from the model Dataset (x and y or icell2d), but update the layer dimension when + neccesary. Parameters ---------- @@ -723,7 +720,6 @@ def col_to_list(col_in, ds, cellids): col_lst : list raster values from ds presented in a list per cell. """ - if isinstance(col_in, str) and col_in in ds: col_in = ds[col_in] if isinstance(col_in, xr.DataArray): @@ -951,7 +947,6 @@ def cols_to_reclist(ds, cellids, *args, cellid_column=0): cellid_column : int, optional Adds the cellid ((layer, row, col) or (layer, icell2d)) to the reclist in this column number. Do not add cellid when cellid_column is None. The default is 0. - """ cols = [col_to_list(col, ds, cellids) for col in args] if cellid_column is not None: @@ -1079,8 +1074,7 @@ def da_to_reclist( def polygon_to_area(modelgrid, polygon, da, gridtype="structured"): - """create a grid with the surface area in each cell based on a polygon - value. + """Create a grid with the surface area in each cell based on a polygon value. Parameters ---------- @@ -1126,8 +1120,8 @@ def polygon_to_area(modelgrid, polygon, da, gridtype="structured"): def gdf_to_data_array_struc( gdf, gwf, field="VALUE", agg_method=None, interp_method=None ): - """Project vector data on a structured grid. Aggregate data if multiple - geometries are in a single cell. + """Project vector data on a structured grid. Aggregate data if multiple geometries + are in a single cell. Parameters ---------- @@ -1192,11 +1186,11 @@ def gdf_to_data_array_struc( def gdf_to_da( - gdf, ds, column, agg_method=None, fill_value=np.NaN, min_total_overlap=0.0, ix=None + gdf, ds, column, agg_method=None, fill_value=np.nan, min_total_overlap=0.0, ix=None ): - """Project vector data on a grid. Aggregate data if multiple - geometries are in a single cell. Supports structured and vertex grids. - This method replaces gdf_to_data_array_struc. + """Project vector data on a grid. Aggregate data if multiple geometries are in a + single cell. Supports structured and vertex grids. This method replaces + gdf_to_data_array_struc. Parameters ---------- @@ -1273,7 +1267,7 @@ def gdf_to_da( def interpolate_gdf_to_array(gdf, gwf, field="values", method="nearest"): - """interpolate data from a point gdf. + """Interpolate data from a point gdf. Parameters ---------- @@ -1462,9 +1456,9 @@ def aggregate_vector_per_cell(gdf, fields_methods, modelgrid=None): def gdf_to_bool_da(gdf, ds, ix=None, buffer=0.0, **kwargs): - """convert a GeoDataFrame with polygon geometries into a data array - corresponding to the modelgrid in which each cell is 1 (True) if one or - more geometries are (partly) in that cell. + """Convert a GeoDataFrame with polygon geometries into a data array corresponding to + the modelgrid in which each cell is 1 (True) if one or more geometries are (partly) + in that cell. Parameters ---------- @@ -1489,9 +1483,9 @@ def gdf_to_bool_da(gdf, ds, ix=None, buffer=0.0, **kwargs): def gdf_to_bool_ds(gdf, ds, da_name, keep_coords=None, ix=None, buffer=0.0, **kwargs): - """convert a GeoDataFrame with polygon geometries into a model dataset with - a data_array named 'da_name' in which each cell is 1 (True) if one or more - geometries are (partly) in that cell. + """Convert a GeoDataFrame with polygon geometries into a model dataset with a + data_array named 'da_name' in which each cell is 1 (True) if one or more geometries + are (partly) in that cell. Parameters ---------- @@ -1703,7 +1697,7 @@ def gdf_to_grid( def get_thickness_from_topbot(top, bot): - """get thickness from data arrays with top and bots. + """Get thickness from data arrays with top and bots. Parameters ---------- @@ -1745,9 +1739,9 @@ def get_thickness_from_topbot(top, bot): def get_vertices_arr(ds, modelgrid=None, vert_per_cid=4, epsilon=0, rotated=False): - """get vertices of a vertex modelgrid from a ds or the modelgrid. Only - return the 4 corners of each cell and not the corners of adjacent cells - thus limiting the vertices per cell to 4 points. + """Get vertices of a vertex modelgrid from a ds or the modelgrid. Only return the 4 + corners of each cell and not the corners of adjacent cells thus limiting the + vertices per cell to 4 points. This method uses the xvertices and yvertices attributes of the modelgrid. When no modelgrid is supplied, a modelgrid-object is created from ds. @@ -1809,9 +1803,9 @@ def get_vertices_arr(ds, modelgrid=None, vert_per_cid=4, epsilon=0, rotated=Fals def get_vertices(ds, vert_per_cid=4, epsilon=0, rotated=False): - """get vertices of a vertex modelgrid from a ds or the modelgrid. Only - return the 4 corners of each cell and not the corners of adjacent cells - thus limiting the vertices per cell to 4 points. + """Get vertices of a vertex modelgrid from a ds or the modelgrid. Only return the 4 + corners of each cell and not the corners of adjacent cells thus limiting the + vertices per cell to 4 points. This method uses the xvertices and yvertices attributes of the modelgrid. When no modelgrid is supplied, a modelgrid-object is created from ds. @@ -1842,7 +1836,6 @@ def get_vertices(ds, vert_per_cid=4, epsilon=0, rotated=False): vertices_da : xarray DataArray Vertex coördinates per cell with dimensions(cid, no_vert, 2). """ - # obtain vertices_arr = get_vertices_arr( @@ -1863,8 +1856,8 @@ def get_vertices(ds, vert_per_cid=4, epsilon=0, rotated=False): @cache.cache_netcdf(coords_2d=True) def mask_model_edge(ds, idomain=None): - """get data array which is 1 for every active cell (defined by idomain) at - the boundaries of the model (xmin, xmax, ymin, ymax). Other cells are 0. + """Get data array which is 1 for every active cell (defined by idomain) at the + boundaries of the model (xmin, xmax, ymin, ymax). Other cells are 0. Parameters ---------- @@ -1940,7 +1933,6 @@ def polygons_from_model_ds(model_ds): polygons : list of shapely Polygons list with polygon of each raster cell. """ - if model_ds.gridtype == "structured": delr = get_delr(model_ds) delc = get_delc(model_ds) diff --git a/nlmod/dims/layers.py b/nlmod/dims/layers.py index a3c44d7b..c468582f 100644 --- a/nlmod/dims/layers.py +++ b/nlmod/dims/layers.py @@ -49,7 +49,7 @@ def calculate_thickness(ds, top="top", bot="botm"): raise ValueError("2d top should have same last dimension as bot") # subtracting floats can result in rounding errors. Mainly anoying for zero thickness layers. - thickness = thickness.where(~np.isclose(thickness, 0.), 0.) + thickness = thickness.where(~np.isclose(thickness, 0.0), 0.0) if isinstance(ds[bot], xr.DataArray): thickness.name = "thickness" @@ -69,8 +69,8 @@ def calculate_thickness(ds, top="top", bot="botm"): def calculate_transmissivity( ds, kh="kh", thickness="thickness", top="top", botm="botm" ): - """calculate the transmissivity (T) as the product of the horizontal - conductance (kh) and the thickness (D). + """Calculate the transmissivity (T) as the product of the horizontal conductance + (kh) and the thickness (D). Parameters ---------- @@ -94,7 +94,6 @@ def calculate_transmissivity( T : xarray.DataArray DataArray containing transmissivity (T). NaN where layer thickness is zero """ - if thickness in ds: thickness = ds[thickness] else: @@ -123,7 +122,7 @@ def calculate_transmissivity( def calculate_resistance(ds, kv="kv", thickness="thickness", top="top", botm="botm"): - """calculate vertical resistance (c) between model layers from the vertical + """Calculate vertical resistance (c) between model layers from the vertical conductivity (kv) and the thickness. The resistance between two layers is assigned to the top layer. The bottom model layer gets a resistance of infinity. @@ -149,7 +148,6 @@ def calculate_resistance(ds, kv="kv", thickness="thickness", top="top", botm="bo c : xarray.DataArray DataArray containing vertical resistance (c). NaN where layer thickness is zero """ - if thickness in ds: thickness = ds[thickness] else: @@ -224,7 +222,6 @@ def split_layers_ds( Dataset with new tops and bottoms taking into account split layers, and filled data for other variables. """ - layers = list(ds.layer.data) # Work on a shallow copy of split_dict @@ -569,7 +566,6 @@ def combine_layers_ds( Dataset with new tops and bottoms taking into account combined layers, and recalculated values for parameters (kh, kv, kD, c). """ - data_vars = [] for dv in [kh, kv, kD, c]: if dv is not None: @@ -644,7 +640,7 @@ def combine_layers_ds( def add_kh_kv_from_ml_layer_to_ds( ml_layer_ds, ds, anisotropy, fill_value_kh, fill_value_kv ): - """add kh and kv from a model layer dataset to the model dataset. + """Add kh and kv from a model layer dataset to the model dataset. Supports structured and vertex grids. @@ -811,8 +807,9 @@ def set_layer_thickness(ds, layer, thickness, change="botm", copy=True): def set_minimum_layer_thickness(ds, layer, min_thickness, change="botm", copy=True): - """Make sure layer has a minimum thickness by lowering the botm of the - layer where neccesary.""" + """Make sure layer has a minimum thickness by lowering the botm of the layer where + neccesary. + """ assert layer in ds.layer assert change == "botm", "Only change=botm allowed for now" if copy: @@ -834,8 +831,8 @@ def set_minimum_layer_thickness(ds, layer, min_thickness, change="botm", copy=Tr def remove_thin_layers( ds, min_thickness=0.1, update_thickness_every_layer=False, copy=True ): - """ - Remove cells with a thickness less than min_thickness (setting the thickness to 0) + """Remove cells with a thickness less than min_thickness (setting the thickness to + 0) The thickness of the removed cells is added to the first active layer below @@ -855,11 +852,11 @@ def remove_thin_layers( copy : bool, optional If copy=True, data in the return value is always copied, so the original Dataset is not altered. The default is True. + Returns ------- ds : xr.Dataset Dataset containing information about layers. - """ if "layer" in ds["top"].dims: msg = "remove_thin_layers does not support top with a layer dimension" @@ -904,8 +901,8 @@ def remove_thin_layers( def get_kh_kv(kh, kv, anisotropy, fill_value_kh=1.0, fill_value_kv=0.1, idomain=None): - """create kh en kv grid data for flopy from existing kh, kv and anistropy - grids with nan values (typically from REGIS). + """Create kh en kv grid data for flopy from existing kh, kv and anistropy grids with + nan values (typically from REGIS). fill nans in kh grid in these steps: 1. take kv and multiply by anisotropy, if this is nan: @@ -1018,7 +1015,6 @@ def fill_top_bot_kh_kv_at_mask(ds, fill_mask): ds : xr.DataSet model dataset with adjusted data variables: 'top', 'botm', 'kh', 'kv' """ - # zee cellen hebben altijd een top gelijk aan 0 ds["top"].values = np.where(fill_mask, 0, ds["top"]) @@ -1065,7 +1061,6 @@ def fill_nan_top_botm_kh_kv( 2. Remove inactive layers, with no positive thickness anywhere 3. Compute kh and kv, filling nans with anisotropy or fill_values """ - # 1 ds = remove_layer_dim_from_top(ds) @@ -1088,8 +1083,7 @@ def fill_nan_top_botm_kh_kv( def fill_nan_top_botm(ds): - """ - Remove Nans in non-existent layers in botm and top variables + """Remove Nans in non-existent layers in botm and top variables. The NaNs are removed by setting the value to the top and botm of higher/lower layers that do exist. @@ -1122,8 +1116,7 @@ def fill_nan_top_botm(ds): def set_nan_top_and_botm(ds, copy=True): - """ - Sets Nans for non-existent layers in botm and top variables + """Sets Nans for non-existent layers in botm and top variables. Nans are only added to top when it contains a layer dimension. @@ -1159,8 +1152,7 @@ def remove_layer_dim_from_top( return_inconsistencies=False, copy=True, ): - """ - Change top from 3d to 2d, removing NaNs in top and botm in the process. + """Change top from 3d to 2d, removing NaNs in top and botm in the process. This method sets variable `top` to the top of the upper layer (like the definition in MODFLOW). This removes redundant data, as the top of all layers exept the most @@ -1220,8 +1212,7 @@ def remove_layer_dim_from_top( def add_layer_dim_to_top(ds, set_non_existing_layers_to_nan=True, copy=True): - """ - Change top from 2d to 3d, setting top and botm to NaN for non-existent layers. + """Change top from 2d to 3d, setting top and botm to NaN for non-existent layers. Parameters ---------- @@ -1248,28 +1239,23 @@ def add_layer_dim_to_top(ds, set_non_existing_layers_to_nan=True, copy=True): def convert_to_modflow_top_bot(ds, **kwargs): - """ - Removes the layer dimension from top and fills nans in top and botm. + """Removes the layer dimension from top and fills nans in top and botm. Alias to remove_layer_dim_from_top - """ ds = remove_layer_dim_from_top(ds, **kwargs) def convert_to_regis_top_bot(ds, **kwargs): - """ - Adds a layer dimension to top and sets non-existing cells to nan in top and botm. + """Adds a layer dimension to top and sets non-existing cells to nan in top and botm. Alias to add_layer_dim_to_top - """ ds = add_layer_dim_to_top(ds, **kwargs) def remove_inactive_layers(ds): - """ - Remove layers which only contain inactive cells + """Remove layers which only contain inactive cells. Parameters ---------- @@ -1280,7 +1266,6 @@ def remove_inactive_layers(ds): ------- ds : xr.Dataset The model Dataset without inactive layers. - """ idomain = get_idomain(ds) # only keep layers with at least one active cell @@ -1314,8 +1299,9 @@ def get_idomain(ds): idomain.attrs.clear() # set idomain of cells with a positive thickness to 1 thickness = calculate_thickness(ds) - # subtracting floats can result in rounding errors. Mainly anoying for zero thickness layers. - thickness = thickness.where(~np.isclose(thickness, 0.), 0.) + # subtracting floats can result in rounding errors. Mainly anoying for zero + # thickness layers. + thickness = thickness.where(~np.isclose(thickness, 0.0), 0.0) idomain.data[thickness.data > 0.0] = 1 # set idomain above/below the first/last active layer to 0 idomain.data[idomain.where(idomain > 0).ffill(dim="layer").isnull()] = 0 @@ -1347,7 +1333,7 @@ def get_first_active_layer(ds, **kwargs): def get_first_active_layer_from_idomain(idomain, nodata=-999): - """get the first (top) active layer in each cell from the idomain. + """Get the first (top) active layer in each cell from the idomain. Parameters ---------- @@ -1377,7 +1363,7 @@ def get_first_active_layer_from_idomain(idomain, nodata=-999): def get_last_active_layer_from_idomain(idomain, nodata=-999): - """get the last (bottom) active layer in each cell from the idomain. + """Get the last (bottom) active layer in each cell from the idomain. Parameters ---------- @@ -1444,7 +1430,7 @@ def get_layer_of_z(ds, z, above_model=-999, below_model=-999): def update_idomain_from_thickness(idomain, thickness, mask): - """get new idomain from thickness in the cells where mask is 1 (or True). + """Get new idomain from thickness in the cells where mask is 1 (or True). Idomain becomes: - 1: if cell thickness is bigger than 0 @@ -1517,7 +1503,7 @@ def aggregate_by_weighted_mean_to_ds(ds, source_ds, var_name): ValueError if source_ds does not have a layer dimension - See also + See Also -------- nlmod.read.geotop.aggregate_to_ds """ @@ -1559,7 +1545,7 @@ def aggregate_by_weighted_mean_to_ds(ds, source_ds, var_name): def check_elevations_consistency(ds): if "layer" in ds["top"].dims: tops = ds["top"].data - top_ref = np.full(tops.shape[1:], np.NaN) + top_ref = np.full(tops.shape[1:], np.nan) for lay, layer in zip(range(tops.shape[0]), ds.layer.data): top = tops[lay] mask = ~np.isnan(top) @@ -1572,7 +1558,7 @@ def check_elevations_consistency(ds): top_ref[mask] = top[mask] bots = ds["botm"].data - bot_ref = np.full(bots.shape[1:], np.NaN) + bot_ref = np.full(bots.shape[1:], np.nan) for lay, layer in zip(range(bots.shape[0]), ds.layer.data): bot = bots[lay] mask = ~np.isnan(bot) @@ -1591,8 +1577,7 @@ def check_elevations_consistency(ds): def insert_layer(ds, name, top, bot, kh=None, kv=None, copy=True): - """ - Inserts a layer in a model Dataset, burning it in an existing layer model. + """Inserts a layer in a model Dataset, burning it in an existing layer model. This method loops over the existing layers, and checks if (part of) the new layer needs to be inserted above the existing layer, and if the top or bottom of the @@ -1652,7 +1637,6 @@ def insert_layer(ds, name, top, bot, kh=None, kv=None, copy=True): ------- ds : xarray.Dataset xarray Dataset containing the new layer(s) - """ shape = ds["botm"].shape[1:] assert top.shape == shape diff --git a/nlmod/dims/rdp.py b/nlmod/dims/rdp.py index 3b24b819..df996789 100644 --- a/nlmod/dims/rdp.py +++ b/nlmod/dims/rdp.py @@ -1,10 +1,11 @@ """ -rdp -~~~ +rdp +~~~ Python implementation of the Ramer-Douglas-Peucker algorithm. -:copyright: 2014-2016 Fabian Hirschmann +:copyright: 2014-2016 Fabian Hirschmann :license: MIT, see LICENSE.txt for more details. """ + import sys from functools import partial @@ -15,8 +16,8 @@ def pldist(point, start, end): - """Calculates the distance from ``point`` to the line given by the points - ``start`` and ``end``. + """Calculates the distance from ``point`` to the line given by the points ``start`` + and ``end``. :param point: a point :type point: numpy array @@ -113,9 +114,8 @@ def rdp_iter(M, epsilon, dist=pldist, return_mask=False): def rdp(M, epsilon=0, dist=pldist, algo="iter", return_mask=False): - """ - Simplifies a given array of points using the Ramer-Douglas-Peucker - algorithm. + """Simplifies a given array of points using the Ramer-Douglas-Peucker algorithm. + Example: >>> from rdp import rdp >>> rdp([[1, 1], [2, 2], [3, 3], [4, 4]]) @@ -153,7 +153,6 @@ def rdp(M, epsilon=0, dist=pldist, algo="iter", return_mask=False): :param return_mask: return mask instead of simplified array :type return_mask: bool """ - if algo == "iter": algo = partial(rdp_iter, return_mask=return_mask) elif algo == "rec": diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index 01725ca7..f6fbf843 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -16,8 +16,7 @@ def get_xy_mid_structured(extent, delr, delc, descending_y=True): - """Calculates the x and y coordinates of the cell centers of a structured - grid. + """Calculates the x and y coordinates of the cell centers of a structured grid. Parameters ---------- @@ -154,8 +153,8 @@ def ds_to_structured_grid( angrot=0.0, method="nearest", ): - """Resample a dataset (xarray) from a structured grid to a new dataset from - a different structured grid. + """Resample a dataset (xarray) from a structured grid to a new dataset from a + different structured grid. Parameters ---------- @@ -193,7 +192,6 @@ def ds_to_structured_grid( dataset with dimensions (layer, y, x). y and x are from the new grid. """ - assert isinstance(ds_in, xr.core.dataset.Dataset) if hasattr(ds_in, "gridtype"): assert ds_in.attrs["gridtype"] == "structured" @@ -226,8 +224,7 @@ def ds_to_structured_grid( def _set_angrot_attributes(extent, xorigin, yorigin, angrot, attrs): - """Internal method to set the properties of the grid in an attribute - dictionary. + """Internal method to set the properties of the grid in an attribute dictionary. Parameters ---------- @@ -285,7 +282,7 @@ def _set_angrot_attributes(extent, xorigin, yorigin, angrot, attrs): def fillnan_da_structured_grid(xar_in, method="nearest"): - """fill not-a-number values in a structured grid, DataArray. + """Fill not-a-number values in a structured grid, DataArray. The fill values are determined using the 'nearest' method of the scipy.interpolate.griddata function @@ -345,7 +342,7 @@ def fillnan_da_structured_grid(xar_in, method="nearest"): def fillnan_da_vertex_grid(xar_in, ds=None, x=None, y=None, method="nearest"): - """fill not-a-number values in a vertex grid, DataArray. + """Fill not-a-number values in a vertex grid, DataArray. The fill values are determined using the 'nearest' method of the scipy.interpolate.griddata function @@ -375,7 +372,6 @@ def fillnan_da_vertex_grid(xar_in, ds=None, x=None, y=None, method="nearest"): ----- can be slow if the xar_in is a large raster """ - if xar_in.dims != ("icell2d",): raise ValueError( f"expected dataarray with dimensions ('icell2d'), got dimensions -> {xar_in.dims}" @@ -407,7 +403,7 @@ def fillnan_da_vertex_grid(xar_in, ds=None, x=None, y=None, method="nearest"): def fillnan_da(da, ds=None, method="nearest"): - """fill not-a-number values in a DataArray. + """Fill not-a-number values in a DataArray. The fill values are determined using the 'nearest' method of the scipy.interpolate.griddata function @@ -457,7 +453,6 @@ def vertex_da_to_ds(da, ds, method="nearest"): xarray.DataArray A DataArray, with the same gridtype as ds. """ - if "icell2d" not in da.dims: return structured_da_to_ds(da, ds, method=method) points = np.array((da.x.data, da.y.data)).T @@ -515,7 +510,7 @@ def dim_to_regular_dim(da, dims, z): return xr.DataArray(z, dims=dims, coords=coords) -def structured_da_to_ds(da, ds, method="average", nodata=np.NaN): +def structured_da_to_ds(da, ds, method="average", nodata=np.nan): """Resample a DataArray to the coordinates of a model dataset. Parameters diff --git a/nlmod/dims/time.py b/nlmod/dims/time.py index f1b21c17..0af4d927 100644 --- a/nlmod/dims/time.py +++ b/nlmod/dims/time.py @@ -75,7 +75,6 @@ def set_ds_time_deprecated( ds : xarray.Dataset dataset with time variant model data """ - warnings.warn( "this function is deprecated and will eventually be removed, " "please use nlmod.time.set_ds_time() in the future.", @@ -200,7 +199,6 @@ def set_ds_time( ------- ds : xarray.Dataset model dataset with added time coordinate - """ logger.info( "Function set_ds_time() has changed since nlmod version 0.7." @@ -280,7 +278,6 @@ def set_ds_time( def ds_time_idx_from_tdis_settings(start, perlen, nstp=1, tsmult=1.0, time_units="D"): """Get time index from TDIS perioddata: perlen, nstp, tsmult. - Parameters ---------- start : str, pd.Timestamp @@ -356,7 +353,6 @@ def estimate_nstp( if `return_dt_arr` is `True` returns the durations of the timesteps corresponding with the returned nstp. """ - nt = len(forcing) # Scaled linear between min and max. array nstp will be modified along the way @@ -409,8 +405,7 @@ def estimate_nstp( def get_time_step_length(perlen, nstp, tsmult): - """ - Get the length of the timesteps within a singe stress-period. + """Get the length of the timesteps within a singe stress-period. Parameters ---------- @@ -426,7 +421,6 @@ def get_time_step_length(perlen, nstp, tsmult): t : np.ndarray An array with the length of each of the timesteps within the stress period, in the same unit as perlen. - """ t = np.array([tsmult**x for x in range(nstp)]) t = t * perlen / t.sum() @@ -455,7 +449,6 @@ def ds_time_idx_from_model(gwf): IndexVariable time coordinate for xarray data-array or dataset """ - return ds_time_idx_from_modeltime(gwf.modeltime) @@ -481,7 +474,6 @@ def ds_time_idx_from_modeltime(modeltime): IndexVariable time coordinate for xarray data-array or dataset """ - return ds_time_idx( np.cumsum(modeltime.perlen), start_datetime=modeltime.start_datetime, diff --git a/nlmod/epsg28992.py b/nlmod/epsg28992.py index 6429fd9b..665cb1a7 100644 --- a/nlmod/epsg28992.py +++ b/nlmod/epsg28992.py @@ -1,6 +1,6 @@ """ NOTE: this is the correct epsg:28992 definition for plotting backgroundmaps in RD -More info (in Dutch) here: +More info (in Dutch) here: https://qgis.nl/2011/12/05/epsg28992-of-rijksdriehoekstelsel-verschuiving/ This was still a problem in October 2023 """ diff --git a/nlmod/gis.py b/nlmod/gis.py index 17d4709c..7c835e55 100644 --- a/nlmod/gis.py +++ b/nlmod/gis.py @@ -5,8 +5,8 @@ import numpy as np from .dims.grid import polygons_from_model_ds -from .dims.resample import get_affine_mod_to_world from .dims.layers import calculate_thickness +from .dims.resample import get_affine_mod_to_world logger = logging.getLogger(__name__) @@ -220,7 +220,6 @@ def ds_to_vector_file( fnames : str or list of str filename(s) of exported geopackage or shapefiles. """ - # get default combination dictionary if combine_dic is None: combine_dic = { @@ -450,7 +449,6 @@ def _break_down_dimension( Copied and altered from imod-python. """ - keep_vars = [] for var in variables: if dim in ds[var].dims: diff --git a/nlmod/gwf/__init__.py b/nlmod/gwf/__init__.py index 5fa031b0..abdc270b 100644 --- a/nlmod/gwf/__init__.py +++ b/nlmod/gwf/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 F403 from . import output, surface_water, wells from .gwf import * from .horizontal_flow_barrier import * diff --git a/nlmod/gwf/gwf.py b/nlmod/gwf/gwf.py index ce45e29c..e234b5a0 100644 --- a/nlmod/gwf/gwf.py +++ b/nlmod/gwf/gwf.py @@ -1,8 +1,3 @@ -# -*- coding: utf-8 -*- -"""Created on Thu Jan 7 17:20:34 2021. - -@author: oebbe -""" import logging import numbers import warnings @@ -11,8 +6,8 @@ import xarray as xr from ..dims import grid -from ..dims.resample import get_delr, get_delc from ..dims.layers import get_idomain +from ..dims.resample import get_delc, get_delr from ..sim import ims, sim, tdis from ..util import _get_value_from_ds_attr, _get_value_from_ds_datavar from . import recharge @@ -21,7 +16,7 @@ def gwf(ds, sim, under_relaxation=False, **kwargs): - """create groundwater flow model from the model dataset. + """Create groundwater flow model from the model dataset. Parameters ---------- @@ -37,7 +32,6 @@ def gwf(ds, sim, under_relaxation=False, **kwargs): gwf : flopy ModflowGwf groundwaterflow object. """ - # start creating model logger.info("creating mf6 GWF") @@ -63,7 +57,7 @@ def gwf(ds, sim, under_relaxation=False, **kwargs): def dis(ds, gwf, length_units="METERS", pname="dis", **kwargs): - """create discretisation package from the model dataset. + """Create discretisation package from the model dataset. Parameters ---------- @@ -85,7 +79,7 @@ def dis(ds, gwf, length_units="METERS", pname="dis", **kwargs): def _dis(ds, model, length_units="METERS", pname="dis", **kwargs): - """create discretisation package from the model dataset. + """Create discretisation package from the model dataset. Parameters ---------- @@ -164,7 +158,7 @@ def _dis(ds, model, length_units="METERS", pname="dis", **kwargs): def disv(ds, gwf, length_units="METERS", pname="disv", **kwargs): - """create discretisation vertices package from the model dataset. + """Create discretisation vertices package from the model dataset. Parameters ---------- @@ -186,7 +180,7 @@ def disv(ds, gwf, length_units="METERS", pname="disv", **kwargs): def _disv(ds, model, length_units="METERS", pname="disv", **kwargs): - """create discretisation vertices package from the model dataset. + """Create discretisation vertices package from the model dataset. Parameters ---------- @@ -267,7 +261,7 @@ def _disv(ds, model, length_units="METERS", pname="disv", **kwargs): def npf( ds, gwf, k="kh", k33="kv", icelltype=0, save_flows=False, pname="npf", **kwargs ): - """create node property flow package from model dataset. + """Create node property flow package from model dataset. Parameters ---------- @@ -331,7 +325,7 @@ def ghb( layer=None, **kwargs, ): - """create general head boundary from model dataset. + """Create general head boundary from model dataset. Parameters ---------- @@ -414,7 +408,7 @@ def drn( layer=None, **kwargs, ): - """create drain from model dataset. + """Create drain from model dataset. Parameters ---------- @@ -487,7 +481,7 @@ def riv( layer=None, **kwargs, ): - """create river package from model dataset. + """Create river package from model dataset. Parameters ---------- @@ -570,7 +564,7 @@ def chd( layer=0, **kwargs, ): - """create constant head package from model dataset. + """Create constant head package from model dataset. Parameters ---------- @@ -645,7 +639,7 @@ def chd( def ic(ds, gwf, starting_head="starting_head", pname="ic", **kwargs): - """create initial condictions package from model dataset. + """Create initial condictions package from model dataset. Parameters ---------- @@ -689,7 +683,7 @@ def sto( pname="sto", **kwargs, ): - """create storage package from model dataset. + """Create storage package from model dataset. Parameters ---------- @@ -741,8 +735,7 @@ def sto( def surface_drain_from_ds(ds, gwf, resistance, elev="ahn", pname="drn", **kwargs): - """create surface level drain (maaivelddrainage in Dutch) from the model - dataset. + """Create surface level drain (maaivelddrainage in Dutch) from the model dataset. Parameters ---------- @@ -765,7 +758,6 @@ def surface_drain_from_ds(ds, gwf, resistance, elev="ahn", pname="drn", **kwargs drn : flopy ModflowGwfdrn drn package """ - ds.attrs["surface_drn_resistance"] = resistance maskarr = _get_value_from_ds_datavar(ds, "elev", elev, return_da=True) @@ -793,7 +785,7 @@ def surface_drain_from_ds(ds, gwf, resistance, elev="ahn", pname="drn", **kwargs def rch(ds, gwf, pname="rch", **kwargs): - """create recharge package from model dataset. + """Create recharge package from model dataset. Parameters ---------- @@ -817,7 +809,7 @@ def rch(ds, gwf, pname="rch", **kwargs): def evt(ds, gwf, pname="evt", **kwargs): - """create evapotranspiration package from model dataset. + """Create evapotranspiration package from model dataset. Parameters ---------- @@ -842,7 +834,7 @@ def evt(ds, gwf, pname="evt", **kwargs): def uzf(ds, gwf, pname="uzf", **kwargs): - """create unsaturated zone flow package from model dataset. + """Create unsaturated zone flow package from model dataset. Parameters ---------- @@ -886,7 +878,8 @@ def _set_record(out, budget, output="head"): def buy(ds, gwf, pname="buy", **kwargs): - """create buoyancy package from model dataset. + """Create buoyancy package from model dataset. + Parameters ---------- ds : xarray.Dataset @@ -946,7 +939,7 @@ def oc( pname="oc", **kwargs, ): - """create output control package from model dataset. + """Create output control package from model dataset. Parameters ---------- @@ -1026,7 +1019,6 @@ def ds_to_gwf(ds, complexity="SIMPLE", icelltype=0, under_relaxation=False): flopy.mf6.ModflowGwf MODFLOW6 GroundwaterFlow model object. """ - # create simulation mf_sim = sim(ds) diff --git a/nlmod/gwf/horizontal_flow_barrier.py b/nlmod/gwf/horizontal_flow_barrier.py index fe97f823..ffd0f053 100644 --- a/nlmod/gwf/horizontal_flow_barrier.py +++ b/nlmod/gwf/horizontal_flow_barrier.py @@ -9,10 +9,10 @@ def get_hfb_spd(gwf, linestrings, hydchr=1 / 100, depth=None, elevation=None): - """Generate a stress period data for horizontal flow barrier between two - cell nodes, with several limitations. The stress period data can be used - directly in the HFB package of flopy. The hfb is placed at the cell - interface; it follows the sides of the cells. + """Generate a stress period data for horizontal flow barrier between two cell nodes, + with several limitations. The stress period data can be used directly in the HFB + package of flopy. The hfb is placed at the cell interface; it follows the sides of + the cells. The estimation of the cross-sectional area at the interface is pretty crude, as the thickness at the cell interface is just the average of the thicknesses of the two @@ -101,8 +101,8 @@ def get_hfb_spd(gwf, linestrings, hydchr=1 / 100, depth=None, elevation=None): def line2hfb(gdf, gwf, prevent_rings=True, plot=False): - """Obtain the cells with a horizontal flow barrier between them from a - geodataframe with line elements. + """Obtain the cells with a horizontal flow barrier between them from a geodataframe + with line elements. Parameters ---------- @@ -278,7 +278,7 @@ def polygon_to_hfb( def plot_hfb(cellids, gwf, ax=None, color="red", **kwargs): - """plots a horizontal flow barrier. + """Plots a horizontal flow barrier. Parameters ---------- diff --git a/nlmod/gwf/output.py b/nlmod/gwf/output.py index f0d0f356..44523534 100644 --- a/nlmod/gwf/output.py +++ b/nlmod/gwf/output.py @@ -11,10 +11,10 @@ from ..dims.resample import get_affine_world_to_mod from ..mfoutput.mfoutput import ( _get_budget_da, - _get_heads_da, - _get_time_index, _get_flopy_data_object, _get_grb_file, + _get_heads_da, + _get_time_index, ) logger = logging.getLogger(__name__) @@ -56,7 +56,6 @@ def get_heads_da( ): """Read binary heads file. - Parameters ---------- ds : xarray.Dataset @@ -202,9 +201,8 @@ def get_budget_da( def get_gwl_from_wet_cells(head, layer="layer", botm=None): - """Get the groundwater level from a multi-dimensional head array where dry - cells are NaN. This methods finds the most upper non-nan-value of each cell - or timestep. + """Get the groundwater level from a multi-dimensional head array where dry cells are + NaN. This methods finds the most upper non-nan-value of each cell or timestep. Parameters ---------- @@ -249,8 +247,7 @@ def get_gwl_from_wet_cells(head, layer="layer", botm=None): def get_flow_residuals(ds, gwf=None, fname=None, grb_file=None, kstpkper=None): - """ - Get the flow residuals of a MODFLOW 6 simulation. + """Get the flow residuals of a MODFLOW 6 simulation. Parameters ---------- @@ -272,7 +269,6 @@ def get_flow_residuals(ds, gwf=None, fname=None, grb_file=None, kstpkper=None): ------- da : xr.DataArray The flow residual in each cell, in m3/d. - """ if grb_file is None: grb_file = _get_grb_file(ds) @@ -289,7 +285,7 @@ def get_flow_residuals(ds, gwf=None, fname=None, grb_file=None, kstpkper=None): for iflowja in flowja: # residuals.append(flopy.mf6.utils.get_residuals(iflowja, grb_file)) # use our own faster method instead of a for loop: - residual = np.full(grb.shape, np.NaN) + residual = np.full(grb.shape, np.nan) residual.ravel()[mask_active] = iflowja.flatten()[flowja_index] residuals.append(residual) dims = ("time",) + dims @@ -297,7 +293,7 @@ def get_flow_residuals(ds, gwf=None, fname=None, grb_file=None, kstpkper=None): else: # residuals = flopy.mf6.utils.get_residuals(flowja[0], grb_file) # use our own faster method instead of a for loop: - residuals = np.full(grb.shape, np.NaN) + residuals = np.full(grb.shape, np.nan) residuals.ravel()[mask_active] = flowja[0].flatten()[flowja_index] da = xr.DataArray(residuals, dims=dims, coords=coords) return da @@ -306,8 +302,7 @@ def get_flow_residuals(ds, gwf=None, fname=None, grb_file=None, kstpkper=None): def get_flow_lower_face( ds, gwf=None, fname=None, grb_file=None, kstpkper=None, lays=None ): - """ - Get the flow over the lower face of all model cells + """Get the flow over the lower face of all model cells. The flow Lower Face (flf) used to be written to the budget file in previous versions of MODFLOW. In MODFLOW 6 we determine these flows from the flow-ja-face-records. @@ -335,7 +330,6 @@ def get_flow_lower_face( ------- da : xr.DataArray The flow over the lower face of each cell, in m3/d. - """ if grb_file is None: grb_file = _get_grb_file(ds) @@ -375,7 +369,7 @@ def get_flow_lower_face( flfs = [] for iflowja in flowja: if ds.gridtype == "vertex": - flf = np.full(shape, np.NaN) + flf = np.full(shape, np.nan) mask = flf_index >= 0 flf[mask] = iflowja[0, 0, flf_index[mask]] else: @@ -385,7 +379,7 @@ def get_flow_lower_face( coords = dict(coords) | {"time": _get_time_index(cbf, ds)} else: if ds.gridtype == "vertex": - flfs = np.full(shape, np.NaN) + flfs = np.full(shape, np.nan) mask = flf_index >= 0 flfs[mask] = flowja[0][0, 0, flf_index[mask]] else: diff --git a/nlmod/gwf/recharge.py b/nlmod/gwf/recharge.py index fe06889c..7f54708b 100644 --- a/nlmod/gwf/recharge.py +++ b/nlmod/gwf/recharge.py @@ -20,8 +20,7 @@ def ds_to_rch( gwf, ds, mask=None, pname="rch", recharge="recharge", auxiliary=None, **kwargs ): - """Convert the recharge data in the model dataset to a rch package with - time series. + """Convert the recharge data in the model dataset to a rch package with time series. Parameters ---------- @@ -110,8 +109,8 @@ def ds_to_evt( auxiliary=None, **kwargs, ): - """Convert the evaporation data in the model dataset to a evt package with - time series. + """Convert the evaporation data in the model dataset to a evt package with time + series. Parameters ---------- @@ -141,7 +140,6 @@ def ds_to_evt( Raises ------ - DESCRIPTION. ValueError DESCRIPTION. @@ -542,8 +540,7 @@ def ds_to_uzf( def _get_unique_series(ds, var, pname): - """Get the location and values of unique time series from a variable var in - ds. + """Get the location and values of unique time series from a variable var in ds. Parameters ---------- diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index c61f646e..7c14d8d5 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -10,11 +10,11 @@ from shapely.strtree import STRtree from tqdm import tqdm +from ..cache import cache_pickle from ..dims.grid import gdf_to_grid from ..dims.layers import get_idomain -from ..dims.resample import get_extent_polygon, extent_to_polygon, get_delr, get_delc +from ..dims.resample import extent_to_polygon, get_delc, get_delr, get_extent_polygon from ..read import bgt, waterboard -from ..cache import cache_pickle logger = logging.getLogger(__name__) @@ -42,7 +42,6 @@ def aggregate(gdf, method, ds=None): celldata : pd.DataFrame DataFrame with aggregated surface water parameters per grid cell """ - required_cols = {"stage", "c0", "botm"} missing_cols = required_cols.difference(gdf.columns) if len(missing_cols) > 0: @@ -300,9 +299,10 @@ def estimate_polygon_length(gdf): def distribute_cond_over_lays( cond, cellid, rivbot, laytop, laybot, idomain=None, kh=None, stage=None ): - """Distribute the conductance in a cell over the layers in that cell, based - on the the river-bottom and the layer bottoms, and optionally based on the - stage and the hydraulic conductivity.""" + """Distribute the conductance in a cell over the layers in that cell, based on the + the river-bottom and the layer bottoms, and optionally based on the stage and the + hydraulic conductivity. + """ if isinstance(rivbot, (np.ndarray, xr.DataArray)): rivbot = float(rivbot[cellid]) if len(laybot.shape) == 3: @@ -393,7 +393,6 @@ def build_spd( - DRN: [(cellid), elev, cond] - GHB: [(cellid), elev, cond] """ - spd = [] top = ds.top.data @@ -517,7 +516,9 @@ def add_info_to_gdf( geom_type="Polygon", add_index_from_column=None, ): - """Add information from 'gdf_from' to 'gdf_to', based on the spatial intersection.""" + """Add information from 'gdf_from' to 'gdf_to', based on the spatial + intersection. + """ gdf_to = gdf_to.copy() if columns is None: columns = gdf_from.columns[~gdf_from.columns.isin(gdf_to.columns)] @@ -702,7 +703,7 @@ def download_watercourses( def _get_waterboard_selection(gdf=None, extent=None, config=None): - """Internal method to select waterboards to get data from""" + """Internal method to select waterboards to get data from.""" if config is None: config = waterboard.get_configuration() if gdf is None and extent is None: @@ -759,7 +760,7 @@ def add_stages_from_waterboards( la = download_level_areas(gdf, extent=extent, config=config) if columns is None: columns = ["summer_stage", "winter_stage"] - gdf[columns] = np.NaN + gdf[columns] = np.nan for wb in la.keys(): if len(la[wb]) == 0: continue @@ -813,7 +814,7 @@ def add_bottom_height_from_waterboards( wc = download_watercourses(gdf, extent=extent, config=config) if columns is None: columns = ["bottom_height"] - gdf[columns] = np.NaN + gdf[columns] = np.nan for wb in wc.keys(): if len(wc[wb]) == 0: continue @@ -883,8 +884,8 @@ def get_gdf(ds=None, extent=None, fname_ahn=None, ahn=None, buffer=0.0): def add_min_ahn_to_gdf(gdf, ahn, buffer=0.0, column="ahn_min"): - """Add a column names with the minimum surface level height near surface - water features. + """Add a column names with the minimum surface level height near surface water + features. Parameters ---------- @@ -905,7 +906,6 @@ def add_min_ahn_to_gdf(gdf, ahn, buffer=0.0, column="ahn_min"): A GeoDataFrame with surface water features, with an added column containing the minimum surface level height near the features. """ - from geocube.api.core import make_geocube from geocube.rasterize import rasterize_image @@ -995,7 +995,7 @@ def gdf_to_seasonal_pkg( # make sure we have a bottom height if "rbot" not in gdf: - gdf["rbot"] = np.NaN + gdf["rbot"] = np.nan mask = gdf["rbot"].isna() if mask.any(): logger.info( diff --git a/nlmod/gwf/wells.py b/nlmod/gwf/wells.py index 3c8f0ced..0e22fe10 100644 --- a/nlmod/gwf/wells.py +++ b/nlmod/gwf/wells.py @@ -24,8 +24,7 @@ def wel_from_df( auxmultname="multiplier", **kwargs, ): - """ - Add a Well (WEL) package based on input from a (Geo)DataFrame. + """Add a Well (WEL) package based on input from a (Geo)DataFrame. Parameters ---------- @@ -70,7 +69,6 @@ def wel_from_df( ------- wel : flopy.mf6.ModflowGwfwel wel package. - """ if aux is None: aux = [] @@ -140,8 +138,7 @@ def maw_from_df( ds=None, **kwargs, ): - """ - Add a Multi-AquiferWell (MAW) package based on input from a (Geo)DataFrame. + """Add a Multi-AquiferWell (MAW) package based on input from a (Geo)DataFrame. Parameters ---------- @@ -188,7 +185,6 @@ def maw_from_df( ------- wel : flopy.mf6.ModflowGwfmaw maw package. - """ if aux is None: aux = [] @@ -267,8 +263,7 @@ def maw_from_df( def _add_cellid(df, ds=None, gwf=None, x="x", y="y"): - """ - Intersect a DataFrame of point Data with the model grid, and add cellid-column. + """Intersect a DataFrame of point Data with the model grid, and add cellid-column. Parameters ---------- @@ -290,7 +285,6 @@ def _add_cellid(df, ds=None, gwf=None, x="x", y="y"): df : gpd.GeoDataFrame A GeoDataFrame with a column named cellid that contains the icell2d-number (vertex-grid) or (row, column) (structured grid). - """ if not isinstance(df, gpd.GeoDataFrame): df = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df[x], df[y])) @@ -300,8 +294,7 @@ def _add_cellid(df, ds=None, gwf=None, x="x", y="y"): def _get_layer_multiplier_for_wells(df, top, botm, ds=None, gwf=None): - """ - Get factors (pandas.DataFrame) for each layer that well screens intersects with. + """Get factors (pandas.DataFrame) for each layer that well screens intersects with. Parameters ---------- @@ -322,7 +315,6 @@ def _get_layer_multiplier_for_wells(df, top, botm, ds=None, gwf=None): multipliers : pd.DataFrame A DataFrame containg the multiplication factors, with the layers as the index and the name of the well screens (the index of df) as columns. - """ # get required data either from gwf or ds if ds is not None: @@ -351,8 +343,7 @@ def _get_layer_multiplier_for_wells(df, top, botm, ds=None, gwf=None): def _get_layer_multiplier_for_well(cid, well_top, well_bot, ml_top, ml_bot, ml_kh): - """ - Get a factor (numpy array) for each layer that a well screen intersects with. + """Get a factor (numpy array) for each layer that a well screen intersects with. Parameters ---------- @@ -373,7 +364,6 @@ def _get_layer_multiplier_for_well(cid, well_top, well_bot, ml_top, ml_bot, ml_k ------- multiplier : numpy array An array with a factor (between 0 and 1) for each of the model layers. - """ # keep the tops and botms of the cell where the well is in ml_top_cid = ml_top[cid].copy() diff --git a/nlmod/gwt/__init__.py b/nlmod/gwt/__init__.py index 78096246..869b583f 100644 --- a/nlmod/gwt/__init__.py +++ b/nlmod/gwt/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 F403 from . import output, prepare from .gwt import * from .output import * diff --git a/nlmod/gwt/gwt.py b/nlmod/gwt/gwt.py index 7311715a..5ae02377 100644 --- a/nlmod/gwt/gwt.py +++ b/nlmod/gwt/gwt.py @@ -11,7 +11,7 @@ def gwt(ds, sim, modelname=None, **kwargs): - """create groundwater transport model from the model dataset. + """Create groundwater transport model from the model dataset. Parameters ---------- @@ -29,7 +29,6 @@ def gwt(ds, sim, modelname=None, **kwargs): gwt : flopy ModflowGwt groundwater transport object. """ - # start creating model logger.info("creating mf6 GWT") @@ -46,7 +45,7 @@ def gwt(ds, sim, modelname=None, **kwargs): def dis(ds, gwt, length_units="METERS", pname="dis", **kwargs): - """create discretisation package from the model dataset. + """Create discretisation package from the model dataset. Parameters ---------- @@ -68,7 +67,7 @@ def dis(ds, gwt, length_units="METERS", pname="dis", **kwargs): def disv(ds, gwt, length_units="METERS", pname="disv", **kwargs): - """create discretisation vertices package from the model dataset. + """Create discretisation vertices package from the model dataset. Parameters ---------- @@ -90,7 +89,7 @@ def disv(ds, gwt, length_units="METERS", pname="disv", **kwargs): def adv(ds, gwt, scheme=None, **kwargs): - """create advection package for groundwater transport model. + """Create advection package for groundwater transport model. Parameters ---------- @@ -114,7 +113,7 @@ def adv(ds, gwt, scheme=None, **kwargs): def dsp(ds, gwt, **kwargs): - """create dispersion package for groundwater transport model. + """Create dispersion package for groundwater transport model. Parameters ---------- @@ -139,7 +138,7 @@ def dsp(ds, gwt, **kwargs): def ssm(ds, gwt, sources=None, **kwargs): - """create source-sink mixing package for groundwater transport model. + """Create source-sink mixing package for groundwater transport model. Parameters ---------- @@ -177,7 +176,7 @@ def ssm(ds, gwt, sources=None, **kwargs): def mst(ds, gwt, porosity=None, **kwargs): - """create mass storage transfer package for groundwater transport model. + """Create mass storage transfer package for groundwater transport model. Parameters ---------- @@ -213,7 +212,7 @@ def mst(ds, gwt, porosity=None, **kwargs): def cnc(ds, gwt, da_mask, da_conc, pname="cnc", **kwargs): - """create constant concentration package for groundwater transport model. + """Create constant concentration package for groundwater transport model. Parameters ---------- @@ -251,7 +250,7 @@ def oc( pname="oc", **kwargs, ): - """create output control package for groundwater transport model. + """Create output control package for groundwater transport model. Parameters ---------- @@ -290,7 +289,7 @@ def oc( def ic(ds, gwt, strt, pname="ic", **kwargs): - """create initial condictions package for groundwater transport model. + """Create initial condictions package for groundwater transport model. Parameters ---------- @@ -319,7 +318,7 @@ def ic(ds, gwt, strt, pname="ic", **kwargs): def gwfgwt(ds, sim, exgtype="GWF6-GWT6", **kwargs): - """create GWF-GWT exchange package for mf6 simulation. + """Create GWF-GWT exchange package for mf6 simulation. Parameters ---------- diff --git a/nlmod/gwt/output.py b/nlmod/gwt/output.py index 955c2061..5dfd76b5 100644 --- a/nlmod/gwt/output.py +++ b/nlmod/gwt/output.py @@ -4,7 +4,7 @@ import xarray as xr from ..dims.layers import calculate_thickness -from ..mfoutput.mfoutput import _get_heads_da, _get_time_index, _get_flopy_data_object +from ..mfoutput.mfoutput import _get_flopy_data_object, _get_heads_da, _get_time_index logger = logging.getLogger(__name__) @@ -91,9 +91,9 @@ def get_concentration_da( def get_concentration_at_gw_surface(conc, layer="layer"): - """Get the concentration level from a multi-dimensional concentration array - where dry or inactive cells are NaN. This methods finds the most upper non- - nan-value of each cell or timestep. + """Get the concentration level from a multi-dimensional concentration array where + dry or inactive cells are NaN. This methods finds the most upper non- nan-value of + each cell or timestep. Parameters ---------- @@ -137,8 +137,8 @@ def get_concentration_at_gw_surface(conc, layer="layer"): def freshwater_head(ds, hp, conc, denseref=None, drhodc=None): - """Calculate equivalent freshwater head from point water heads. - Heads file produced by mf6 contains point water heads. + """Calculate equivalent freshwater head from point water heads. Heads file produced + by mf6 contains point water heads. Parameters ---------- @@ -180,8 +180,8 @@ def freshwater_head(ds, hp, conc, denseref=None, drhodc=None): def pointwater_head(ds, hf, conc, denseref=None, drhodc=None): - """Calculate point water head from freshwater heads. - Heads file produced by mf6 contains point water heads. + """Calculate point water head from freshwater heads. Heads file produced by mf6 + contains point water heads. Parameters ---------- diff --git a/nlmod/mfoutput/mfoutput.py b/nlmod/mfoutput/mfoutput.py index f7b21ed5..a2771f86 100644 --- a/nlmod/mfoutput/mfoutput.py +++ b/nlmod/mfoutput/mfoutput.py @@ -1,11 +1,10 @@ -import os import logging +import os import warnings import dask -import xarray as xr - import flopy +import xarray as xr from ..dims.grid import get_dims_coords_from_modelgrid, modelgrid_from_ds from ..dims.resample import get_affine_mod_to_world @@ -243,7 +242,7 @@ def _get_flopy_data_object( var, ds=None, gwml=None, fname=None, grb_file=None, **kwargs ): """Get modflow HeadFile or CellBudgetFile object, containg heads, budgets or - concentrations + concentrations. Provide one of ds, gwf or fname. diff --git a/nlmod/modpath/__init__.py b/nlmod/modpath/__init__.py index 396e6362..0154abca 100644 --- a/nlmod/modpath/__init__.py +++ b/nlmod/modpath/__init__.py @@ -1 +1,2 @@ +# ruff: noqa: F403 from .modpath import * diff --git a/nlmod/modpath/modpath.py b/nlmod/modpath/modpath.py index 336db050..37c74409 100644 --- a/nlmod/modpath/modpath.py +++ b/nlmod/modpath/modpath.py @@ -16,8 +16,8 @@ def write_and_run(mpf, remove_prev_output=True, script_path=None, silent=False): - """write modpath files and run the model. Extra options include removing - previous output and copying the modelscript to the model workspace. + """Write modpath files and run the model. Extra options include removing previous + output and copying the modelscript to the model workspace. Parameters ---------- @@ -55,9 +55,8 @@ def write_and_run(mpf, remove_prev_output=True, script_path=None, silent=False): def xy_to_nodes(xy_list, mpf, ds, layer=0): - """convert a list of points, defined by x and y coordinates, to a list of - nodes. A node is a unique cell in a model. The icell2d is a unique cell in - a layer. + """Convert a list of points, defined by x and y coordinates, to a list of nodes. A + node is a unique cell in a model. The icell2d is a unique cell in a layer. Parameters ---------- @@ -145,7 +144,7 @@ def package_to_nodes(gwf, package_name, mpf): def layer_to_nodes(mpf, modellayer): - """get the nodes of all cells in one ore more model layer(s). + """Get the nodes of all cells in one ore more model layer(s). Parameters ---------- @@ -264,16 +263,14 @@ def bas(mpf, porosity=0.3, **kwargs): mpfbas : flopy.modpath.mp7bas.Modpath7Bas modpath bas package. """ - mpfbas = flopy.modpath.Modpath7Bas(mpf, porosity=porosity, **kwargs) return mpfbas def remove_output(mpf): - """Remove the output of a previous modpath run. Commonly used before - starting a new modpath run to avoid loading the wrong data when a modpath - run has failed. + """Remove the output of a previous modpath run. Commonly used before starting a new + modpath run to avoid loading the wrong data when a modpath run has failed. Parameters ---------- @@ -456,6 +453,7 @@ def sim( simulationtype="combined", weaksinkoption="pass_through", weaksourceoption="pass_through", + **kwargs, ): """Create a modpath backward simulation from a particle group. @@ -505,6 +503,7 @@ def sim( stoptimeoption=stoptimeoption, stoptime=stoptime, particlegroups=particlegroups, + **kwargs, ) return mpsim diff --git a/nlmod/plot/__init__.py b/nlmod/plot/__init__.py index ffeb6bd0..e77e6c9b 100644 --- a/nlmod/plot/__init__.py +++ b/nlmod/plot/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 from . import flopy from .dcs import DatasetCrossSection from .plot import ( @@ -7,6 +8,7 @@ geotop_lithok_in_cross_section, geotop_lithok_on_map, map_array, + # modelextent, modelgrid, surface_water, ) diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index f27162b0..cc44ed24 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -1,14 +1,13 @@ -import flopy import logging -import matplotlib +from functools import partial +import flopy import geopandas as gpd +import matplotlib import matplotlib.pyplot as plt import numpy as np import pandas as pd import xarray as xr - -from functools import partial from matplotlib.animation import FFMpegWriter, FuncAnimation from matplotlib.collections import LineCollection, PatchCollection from matplotlib.patches import Rectangle @@ -19,7 +18,6 @@ from ..dims.resample import get_affine_world_to_mod from .plotutil import get_map - logger = logging.getLogger(__name__) @@ -342,7 +340,7 @@ def plot_map_cs( label="cross section", **kwargs, ): - """Creates a different figure with the map of the cross section + """Creates a different figure with the map of the cross section. Parameters ---------- @@ -365,7 +363,6 @@ def plot_map_cs( matplotlib Axes axes """ - if ax is None: _, ax = get_map( self.ds.extent, background=background, figsize=figsize, **kwargs @@ -376,7 +373,7 @@ def plot_map_cs( return ax def get_patches_array(self, z): - """similar to plot_array function, only computes the array to update an existing plot_array. + """Similar to plot_array function, only computes the array to update an existing plot_array. Parameters ---------- @@ -525,7 +522,7 @@ def animate( cbar_label=None, fname=None, ): - """animate a cross section + """Animate a cross section. Parameters ---------- @@ -553,7 +550,6 @@ def animate( matplotlib.animation.FuncAnimation animation object """ - f = self.ax.get_figure() # plot first timeframe diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index 7ff8d0fe..66fb315e 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -2,10 +2,10 @@ from functools import partial import flopy as fp -import xarray as xr import matplotlib.pyplot as plt import numpy as np import pandas as pd +import xarray as xr from matplotlib.animation import FFMpegWriter, FuncAnimation from matplotlib.collections import PatchCollection from matplotlib.colors import ListedColormap, Normalize @@ -59,7 +59,7 @@ def facet_plot( xlim=None, ylim=None, ): - """make a 2d plot of every modellayer, store them in a grid. + """Make a 2d plot of every modellayer, store them in a grid. Parameters ---------- @@ -92,7 +92,6 @@ def facet_plot( axes : TYPE DESCRIPTION. """ - warnings.warn( "this function is out of date and will probably be removed in a future version", DeprecationWarning, @@ -216,7 +215,14 @@ def data_array(da, ds=None, ax=None, rotated=False, edgecolor=None, **kwargs): def geotop_lithok_in_cross_section( - line, gt=None, ax=None, legend=True, legend_loc=None, lithok_props=None, alpha=None, **kwargs + line, + gt=None, + ax=None, + legend=True, + legend_loc=None, + lithok_props=None, + alpha=None, + **kwargs, ): """PLot the lithoclass-data of GeoTOP in a cross-section. @@ -269,7 +275,7 @@ def geotop_lithok_in_cross_section( cs = DatasetCrossSection(gt, line, layer="z", ax=ax, **kwargs) array, cmap, norm = _get_geotop_cmap_and_norm(gt["lithok"], lithok_props) cs.plot_array(array, norm=norm, cmap=cmap, alpha=alpha) - + if legend: # make a legend with dummy handles _add_geotop_lithok_legend(lithok_props, ax, lithok=gt["lithok"], loc=legend_loc) @@ -306,7 +312,6 @@ def geotop_lithok_on_map( Returns ------- qm : matplotlib.collections.QuadMesh - """ if ax is None: ax = plt.gca() @@ -330,7 +335,7 @@ def geotop_lithok_on_map( def _add_geotop_lithok_legend(lithok_props, ax, lithok=None, **kwargs): - """Add a legend with lithok-data""" + """Add a legend with lithok-data.""" handles = [] if lithok is None: lithoks = lithok_props.index @@ -345,10 +350,10 @@ def _add_geotop_lithok_legend(lithok_props, ax, lithok=None, **kwargs): def _get_geotop_cmap_and_norm(lithok, lithok_props): - """Get an array of lithok-values, with a corresponding colormap and norm""" + """Get an array of lithok-values, with a corresponding colormap and norm.""" lithok_un = np.unique(lithok) lithok_un = lithok_un[~np.isnan(lithok_un)] - array = np.full(lithok.shape, np.NaN) + array = np.full(lithok.shape, np.nan) colors = [] for i, ilithok in enumerate(lithok_un): ilithok = int(ilithok) diff --git a/nlmod/plot/plotutil.py b/nlmod/plot/plotutil.py index 00c134e3..67c6c09d 100644 --- a/nlmod/plot/plotutil.py +++ b/nlmod/plot/plotutil.py @@ -186,8 +186,7 @@ def rotate_yticklabels(ax): def rd_ticks(ax, base=1000.0, fmt_base=1000.0, fmt="{:.0f}"): - """Add ticks every 1000 (base) m, and divide ticklabels by 1000 - (fmt_base)""" + """Add ticks every 1000 (base) m, and divide ticklabels by 1000 (fmt_base).""" def fmt_rd_ticks(x, _): return fmt.format(x / fmt_base) diff --git a/nlmod/read/__init__.py b/nlmod/read/__init__.py index f6c7f2ce..5e605993 100644 --- a/nlmod/read/__init__.py +++ b/nlmod/read/__init__.py @@ -1,3 +1,4 @@ +# ruff: noqa: F401 from . import ( administrative, ahn, @@ -9,11 +10,11 @@ knmi, knmi_data_platform, meteobase, + nhi, regis, rws, waterboard, webservices, - nhi, ) from .geotop import get_geotop from .regis import get_regis diff --git a/nlmod/read/ahn.py b/nlmod/read/ahn.py index 1bd2fda8..859f2ceb 100644 --- a/nlmod/read/ahn.py +++ b/nlmod/read/ahn.py @@ -1,10 +1,10 @@ import datetime as dt import logging +import geopandas as gpd import matplotlib.pyplot as plt import numpy as np import pandas as pd -import geopandas as gpd import rasterio import rioxarray import xarray as xr @@ -90,8 +90,7 @@ def get_ahn_at_point( res=0.5, **kwargs, ): - """ - Get the height of the surface level at a certain point, defined by x and y. + """Get the height of the surface level at a certain point, defined by x and y. Parameters ---------- @@ -117,7 +116,6 @@ def get_ahn_at_point( ------- float The surface level value at the requested point. - """ extent = [x - buffer, x + buffer, y - buffer, y + buffer] ahn = get_latest_ahn_from_wcs(extent, identifier=identifier, res=res, **kwargs) @@ -133,8 +131,7 @@ def get_ahn_at_point( def get_ahn_along_line(line, ahn=None, dx=None, num=None, method="linear", plot=False): - """ - Get the height of the surface level along a line. + """Get the height of the surface level along a line. Parameters ---------- @@ -161,7 +158,6 @@ def get_ahn_along_line(line, ahn=None, dx=None, num=None, method="linear", plot= ------- z : xr.DataArray A DataArray with dimension s, containing surface level values along the line. - """ if ahn is None: bbox = line.bounds @@ -236,7 +232,6 @@ def get_latest_ahn_from_wcs( xr.DataArray or MemoryFile DataArray (if as_data_array is True) or Rasterio MemoryFile of the AHN """ - url = "https://service.pdok.nl/rws/ahn/wcs/v1_0?SERVICE=WCS&request=GetCapabilities" if isinstance(extent, xr.DataArray): @@ -275,10 +270,9 @@ def get_latest_ahn_from_wcs( def get_ahn2_tiles(extent=None): """Get the tiles (kaartbladen) of AHN3 as a GeoDataFrame. - The links in the tiles are cuurently incorrect. Thereore - get_ahn3_tiles is used in get_ahn2 and get_ahn1, as the tiles from - get_ahn3_tiles also contain information about the tiles of ahn1 and - ahn2 + The links in the tiles are cuurently incorrect. Thereore get_ahn3_tiles is used in + get_ahn2 and get_ahn1, as the tiles from get_ahn3_tiles also contain information + about the tiles of ahn1 and ahn2 """ url = "https://services.arcgis.com/nSZVuSZjHpEZZbRo/arcgis/rest/services/Kaartbladen_AHN2/FeatureServer" layer = 0 @@ -299,8 +293,7 @@ def get_ahn3_tiles(extent=None): def get_ahn4_tiles(extent=None): - """Get the tiles (kaartbladen) of AHN4 as a GeoDataFrame with download - links.""" + """Get the tiles (kaartbladen) of AHN4 as a GeoDataFrame with download links.""" url = "https://services.arcgis.com/nSZVuSZjHpEZZbRo/arcgis/rest/services/Kaartbladen_AHN4/FeatureServer" layer = 0 gdf = arcrest(url, layer, extent) diff --git a/nlmod/read/bro.py b/nlmod/read/bro.py index fe19dbbf..352c67aa 100644 --- a/nlmod/read/bro.py +++ b/nlmod/read/bro.py @@ -11,7 +11,7 @@ def add_modelled_head(oc, ml=None, ds=None, method="linear"): - """add modelled heads as seperate observations to the ObsCollection. + """Add modelled heads as seperate observations to the ObsCollection. Parameters ---------- @@ -30,7 +30,6 @@ def add_modelled_head(oc, ml=None, ds=None, method="linear"): ObsCollection combination of observed and modelled groundwater heads. """ - oc["modellayer"] = oc.gwobs.get_modellayers(gwf=ml) if ds is not None and "heads" in ds: heads = ds["heads"] @@ -100,7 +99,7 @@ def get_bro( max_screen_top=None, min_screen_bot=None, ): - """get bro groundwater measurements within an extent. + """Get bro groundwater measurements within an extent. Parameters ---------- @@ -168,10 +167,10 @@ def get_bro( @cache.cache_pickle def get_bro_metadata(extent, max_dx=20000, max_dy=20000): - """wrapper around hpd.read_bro that deals with large extents and only - returns metadata (location, tube top/bot, ground level, ..) of the wells - and no actual measurements. This is useful when the extent is too big - to obtain all measurements at once. + """Wrapper around hpd.read_bro that deals with large extents and only returns + metadata (location, tube top/bot, ground level, ..) of the wells and no actual + measurements. This is useful when the extent is too big to obtain all measurements + at once. Parameters ---------- @@ -188,7 +187,6 @@ def get_bro_metadata(extent, max_dx=20000, max_dy=20000): ------- ObsCollection """ - # check if extent is within limits dx = extent[1] - extent[0] dy = extent[3] - extent[2] diff --git a/nlmod/read/geotop.py b/nlmod/read/geotop.py index 536172c1..9457e53a 100644 --- a/nlmod/read/geotop.py +++ b/nlmod/read/geotop.py @@ -103,7 +103,6 @@ def to_model_layers( ds: xr.DataSet dataset with top and botm (and optionally kh and kv) per geotop layer """ - if strat_props is None: strat_props = get_strat_props() @@ -135,8 +134,8 @@ def to_model_layers( geulen = [] for layer, unit in enumerate(units): mask = strat == unit - top[layer] = np.nanmax(np.where(mask, z, np.NaN), 0) + 0.25 - bot[layer] = np.nanmin(np.where(mask, z, np.NaN), 0) - 0.25 + top[layer] = np.nanmax(np.where(mask, z, np.nan), 0) + 0.25 + bot[layer] = np.nanmin(np.where(mask, z, np.nan), 0) - 0.25 if int(unit) in strat_props.index: layers.append(strat_props.at[unit, "code"]) else: @@ -236,8 +235,8 @@ def to_model_layers( @cache.cache_netcdf() def get_geotop(extent, url=GEOTOP_URL, probabilities=False): """Get a slice of the geotop netcdf url within the extent, set the x and y - coordinates to match the cell centers and keep only the strat and lithok - data variables. + coordinates to match the cell centers and keep only the strat and lithok data + variables. Parameters ---------- @@ -319,12 +318,12 @@ def add_top_and_botm(ds): """ bottom = np.expand_dims(ds.z.values - 0.25, axis=(1, 2)) bottom = np.repeat(np.repeat(bottom, len(ds.y), 1), len(ds.x), 2) - bottom[np.isnan(ds.strat.values)] = np.NaN + bottom[np.isnan(ds.strat.values)] = np.nan ds["botm"] = ("z", "y", "x"), bottom top = np.expand_dims(ds.z.values + 0.25, axis=(1, 2)) top = np.repeat(np.repeat(top, len(ds.y), 1), len(ds.x), 2) - top[np.isnan(ds.strat.values)] = np.NaN + top[np.isnan(ds.strat.values)] = np.nan ds["top"] = ("z", "y", "x"), top return ds @@ -380,7 +379,6 @@ def add_kh_and_kv( Raises ------ - DESCRIPTION. Returns @@ -411,8 +409,8 @@ def add_kh_and_kv( if stochastic is None: # calculate kh and kv from most likely lithoclass lithok = gt["lithok"].values - kh_ar = np.full(lithok.shape, np.NaN) - kv_ar = np.full(lithok.shape, np.NaN) + kh_ar = np.full(lithok.shape, np.nan) + kv_ar = np.full(lithok.shape, np.nan) if "strat" in df: combs = np.column_stack((strat.ravel(), lithok.ravel())) # drop nans @@ -445,7 +443,7 @@ def add_kh_and_kv( probality = gt[f"kans_{ilithok}"].values if "strat" in df: khi, kvi = _handle_nans_in_stochastic_approach( - np.NaN, np.NaN, kh_method, kv_method + np.nan, np.nan, kh_method, kv_method ) khi = np.full(strat.shape, khi) kvi = np.full(strat.shape, kvi) @@ -508,7 +506,7 @@ def _get_kh_kv_from_df(df, ilithok, istrat=None, anisotropy=1.0, mask=None): else: msg = f"{msg}. Setting values of {mask.sum()} voxels to NaN." logger.warning(msg) - return np.NaN, np.NaN + return np.nan, np.nan kh = df.loc[mask_df, "kh"].mean() if "kv" in df: @@ -540,8 +538,8 @@ def _handle_nans_in_stochastic_approach(kh, kv, kh_method, kv_method): def aggregate_to_ds( gt, ds, kh="kh", kv="kv", kd="kD", c="c", kh_gt="kh", kv_gt="kv", add_kd_and_c=False ): - """Aggregate voxels from GeoTOP to layers in a model DataSet with top and - botm, to calculate kh and kv. + """Aggregate voxels from GeoTOP to layers in a model DataSet with top and botm, to + calculate kh and kv. Parameters ---------- diff --git a/nlmod/read/jarkus.py b/nlmod/read/jarkus.py index 5d962973..4ffe921c 100644 --- a/nlmod/read/jarkus.py +++ b/nlmod/read/jarkus.py @@ -1,12 +1,13 @@ -"""module with functions to deal with the northsea by: +"""Module with functions to deal with the northsea. - - identifying model cells with the north sea + - identify model cells with the north sea - add bathymetry of the northsea to the layer model - - extrpolate the layer model below the northsea bed. + - extrapolate the layer model below the northsea bed. Note: if you like jazz please check this out: https://www.northseajazz.com """ + import datetime as dt import logging import os @@ -25,7 +26,7 @@ @cache.cache_netcdf() def get_bathymetry(ds, northsea, kind="jarkus", method="average"): - """get bathymetry of the Northsea from the jarkus dataset. + """Get bathymetry of the Northsea from the jarkus dataset. Parameters ---------- @@ -126,7 +127,6 @@ def get_dataset_jarkus(extent, kind="jarkus", return_tiles=False, time=-1): dataset containing bathymetry data """ - extent = [int(x) for x in extent] netcdf_tile_names = get_jarkus_tilenames(extent, kind) diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index b142848c..466641bd 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -15,7 +15,7 @@ @cache.cache_netcdf(coords_3d=True, coords_time=True) def get_recharge(ds, method="linear", most_common_station=False): - """add multiple recharge packages to the groundwater flow model with knmi + """Add multiple recharge packages to the groundwater flow model with knmi data by following these steps: 1. check for each cell (structured or vertex) which knmi measurement stations (prec and evap) are the closest. @@ -50,7 +50,6 @@ def get_recharge(ds, method="linear", most_common_station=False): ds : xr.DataSet dataset with spatial model data including the rch raster """ - if "time" not in ds: raise ( AttributeError( @@ -159,7 +158,7 @@ def _add_ts_to_ds(timeseries, loc_sel, variable, ds): def get_locations_vertex(ds): - """get dataframe with the locations of the grid cells of a vertex grid. + """Get dataframe with the locations of the grid cells of a vertex grid. Parameters ---------- @@ -193,7 +192,7 @@ def get_locations_vertex(ds): def get_locations_structured(ds): - """get dataframe with the locations of the grid cells of a structured grid. + """Get dataframe with the locations of the grid cells of a structured grid. Parameters ---------- @@ -206,7 +205,6 @@ def get_locations_structured(ds): DataFrame with the locations of all active grid cells. includes the columns: x, y, row, col and layer """ - # store x and y mids in locations of active cells fal = get_first_active_layer(ds) rows, columns = np.where(fal != fal.attrs["nodata"]) @@ -228,7 +226,7 @@ def get_locations_structured(ds): def get_knmi_at_locations(ds, start="2010", end=None, most_common_station=False): - """get knmi data at the locations of the active grid cells in ds. + """Get knmi data at the locations of the active grid cells in ds. Parameters ---------- diff --git a/nlmod/read/knmi_data_platform.py b/nlmod/read/knmi_data_platform.py index 44c1b90c..afda1362 100644 --- a/nlmod/read/knmi_data_platform.py +++ b/nlmod/read/knmi_data_platform.py @@ -58,7 +58,7 @@ def get_list_of_files( start_after_filename: Optional[str] = None, timeout: int = 120, ) -> List[str]: - """Download list of files from KNMI data platform""" + """Download list of files from KNMI data platform.""" if api_key is None: api_key = get_anonymous_api_key() files = [] @@ -89,7 +89,7 @@ def download_file( api_key: Optional[str] = None, timeout: int = 120, ) -> None: - """Download file from KNMI data platform""" + """Download file from KNMI data platform.""" if api_key is None: api_key = get_anonymous_api_key() url = ( @@ -119,7 +119,7 @@ def download_files( api_key: Optional[str] = None, timeout: int = 120, ) -> None: - """Download multiple files from KNMI data platform""" + """Download multiple files from KNMI data platform.""" for fname in tqdm(fnames): download_file( dataset_name=dataset_name, @@ -132,7 +132,7 @@ def download_files( def read_nc(fo: Union[str, FileIO], **kwargs: dict) -> xr.Dataset: - """Read netcdf (.nc) file to xarray Dataset""" + """Read netcdf (.nc) file to xarray Dataset.""" # could help to provide argument: engine="h5netcdf" return xr.open_dataset(fo, **kwargs) @@ -161,7 +161,7 @@ def get_timestamp_from_fname(fname: str) -> Union[Timestamp, None]: def add_h5_meta(meta: Dict[str, Any], h5obj: Any, orig_ky: str = "") -> Dict[str, Any]: - """Read metadata from hdf5 (.h5) file and add to existing metadata dictionary""" + """Read metadata from hdf5 (.h5) file and add to existing metadata dictionary.""" def cleanup(val: Any) -> Any: if isinstance(val, (ndarray, list)): @@ -174,7 +174,7 @@ def cleanup(val: Any) -> Any: return val if hasattr(h5obj, "attrs"): - attrs = getattr(h5obj, "attrs") + attrs = h5obj.attrs submeta = {f"{orig_ky}/{ky}": cleanup(val) for ky, val in attrs.items()} meta.update(submeta) @@ -186,7 +186,7 @@ class MultipleDatasetsFound(Exception): def read_h5_contents(h5fo: FileIO) -> Tuple[ndarray, Dict[str, Any]]: - """Read contents from a hdf5 (.h5) file""" + """Read contents from a hdf5 (.h5) file.""" from h5py import Dataset as h5Dataset data = None @@ -206,7 +206,7 @@ def read_h5_contents(h5fo: FileIO) -> Tuple[ndarray, Dict[str, Any]]: def read_h5(fo: Union[str, FileIO]) -> xr.Dataset: - """Read hdf5 (.h5) file to xarray Dataset""" + """Read hdf5 (.h5) file to xarray Dataset.""" from h5py import File as h5File with h5File(fo) as h5fo: @@ -231,7 +231,7 @@ def read_h5(fo: Union[str, FileIO]) -> xr.Dataset: def read_grib( fo: Union[str, FileIO], filter_by_keys=None, **kwargs: dict ) -> xr.Dataset: - """Read GRIB file to xarray Dataset""" + """Read GRIB file to xarray Dataset.""" if kwargs is None: kwargs = {} @@ -248,7 +248,7 @@ def read_grib( def read_dataset_from_zip( fname: str, hour: Optional[int] = None, **kwargs: dict ) -> xr.Dataset: - """Read KNMI data platfrom .zip file to xarray Dataset""" + """Read KNMI data platfrom .zip file to xarray Dataset.""" if fname.endswith(".zip"): with ZipFile(fname) as zipfo: fnames = sorted([x for x in zipfo.namelist() if not x.endswith("/")]) @@ -276,7 +276,7 @@ def read_dataset( hour: Optional[int] = None, **kwargs: dict, ) -> xr.Dataset: - """Read xarray dataset from different file types; .nc, .h5 or grib file""" + """Read xarray dataset from different file types; .nc, .h5 or grib file.""" if hour is not None: if hour == 24: hour = 0 diff --git a/nlmod/read/meteobase.py b/nlmod/read/meteobase.py index a5df07db..5b3a9e11 100644 --- a/nlmod/read/meteobase.py +++ b/nlmod/read/meteobase.py @@ -11,8 +11,7 @@ class MeteobaseType(Enum): - """Enum class to couple folder names to observation type (from in - LEESMIJ.txt)""" + """Enum class to couple folder names to observation type (from in LEESMIJ.txt)""" NEERSLAG = "Neerslagradargegevens in Arc/Info-formaat." MAKKINK = "Verdampingsgegevens volgens Makkink." @@ -56,8 +55,7 @@ def read_leesmij(fo: FileIO) -> Dict[str, Dict[str, str]]: def get_timestamp_from_fname(fname: str) -> Timestamp: - """Get the Timestamp from a filename (with some assumptions about the - formatting)""" + """Get the Timestamp from a filename (with some assumptions about the formatting)""" datestr = re.search("([0-9]{8})", fname) # assumes YYYYMMDD if datestr is not None: match = datestr.group(0) @@ -130,7 +128,7 @@ def read_ascii(fo: FileIO) -> Union[np.ndarray, dict]: def get_xy_from_ascii_meta( - meta: Dict[str, Union[int, float]] + meta: Dict[str, Union[int, float]], ) -> Tuple[np.ndarray, np.ndarray]: """Get the xy coordinates Esri ASCII raster format header. @@ -268,7 +266,6 @@ def read_meteobase( ------- List[DataArray] """ - with ZipFile(Path(path)) as zfile: with zfile.open("LEESMIJ.TXT") as fo: meta = read_leesmij(fo) diff --git a/nlmod/read/nhi.py b/nlmod/read/nhi.py index 0ddaf34b..4e721d60 100644 --- a/nlmod/read/nhi.py +++ b/nlmod/read/nhi.py @@ -1,12 +1,11 @@ +import io import logging import os -import io -import requests +import geopandas as gpd import numpy as np import pandas as pd -import geopandas as gpd - +import requests import rioxarray from ..dims.resample import structured_da_to_ds @@ -15,8 +14,7 @@ def download_file(url, pathname, filename=None, overwrite=False, timeout=120.0): - """ - Download a file from the NHI website. + """Download a file from the NHI website. Parameters ---------- @@ -37,7 +35,6 @@ def download_file(url, pathname, filename=None, overwrite=False, timeout=120.0): ------- fname : str The full path of the downloaded file. - """ if filename is None: filename = url.split("/")[-1] @@ -51,8 +48,7 @@ def download_file(url, pathname, filename=None, overwrite=False, timeout=120.0): def download_buisdrainage(pathname, overwrite=False): - """ - Download resistance and depth of buisdrainage from the NHI website + """Download resistance and depth of buisdrainage from the NHI website. Parameters ---------- @@ -67,7 +63,6 @@ def download_buisdrainage(pathname, overwrite=False): The full path of the downloaded file containing the resistance of buisdrainage. fname_d : str The full path of the downloaded file containing the depth of buisdrainage. - """ url_bas = "https://thredds.data.nhi.nu/thredds/fileServer/opendap/models/nhi3_2/25m" @@ -90,8 +85,7 @@ def add_buisdrainage( cond_method="average", depth_method="mode", ): - """ - Add data about the buisdrainage to the model Dataset. + """Add data about the buisdrainage to the model Dataset. This data consists of the conductance of buisdrainage (m2/d) and the depth of buisdrainage (m to surface level). With the default settings for `cond_method` and @@ -129,7 +123,6 @@ def add_buisdrainage( ds : xr.Dataset The model dataset with added variables with the names `cond_var` and `depth_var`. - """ if pathname is None: pathname = ds.cachedir @@ -190,8 +183,7 @@ def get_gwo_wells( timeout=120, **kwargs, ): - """ - Get metadata of extraction wells from the NHI GWO database + """Get metadata of extraction wells from the NHI GWO database. Parameters ---------- @@ -228,7 +220,6 @@ def get_gwo_wells( ------- gdf : geopandas.GeoDataFrame A GeoDataFrame containing the properties of the wells and their filters. - """ # zie https://gwo.nhi.nu/api/v1/download/ url = "https://gwo.nhi.nu/api/v1/well_filters/" @@ -284,8 +275,7 @@ def get_gwo_measurements( timeout=120, **kwargs, ): - """ - Get extraction rates and metadata of wells from the NHI GWO database + """Get extraction rates and metadata of wells from the NHI GWO database. Parameters ---------- @@ -321,7 +311,6 @@ def get_gwo_measurements( A DataFrame containing the extraction rates of the wells in the database. gdf : geopandas.GeoDataFrame A GeoDataFrame containing the properties of the wells and their filters. - """ url = "http://gwo.nhi.nu/api/v1/measurements/" properties = [] diff --git a/nlmod/read/regis.py b/nlmod/read/regis.py index 1f27c6c7..f1ba8683 100644 --- a/nlmod/read/regis.py +++ b/nlmod/read/regis.py @@ -25,7 +25,7 @@ def get_combined_layer_models( geotop_layers="HLc", geotop_k=None, ): - """combine layer models into a single layer model. + """Combine layer models into a single layer model. Possibilities so far include: - use_regis -> full model based on regis @@ -64,7 +64,6 @@ def get_combined_layer_models( ValueError if an invalid combination of layers is used. """ - if use_regis: regis_ds = get_regis( extent, regis_botm_layer, remove_nan_layers=remove_nan_layers @@ -101,7 +100,7 @@ def get_regis( drop_layer_dim_from_top=True, probabilities=False, ): - """get a regis dataset projected on the modelgrid. + """Get a regis dataset projected on the modelgrid. Parameters ---------- @@ -132,7 +131,6 @@ def get_regis( regis_ds : xarray dataset dataset with regis data projected on the modelgrid. """ - ds = xr.open_dataset(REGIS_URL, decode_times=False) # set x and y dimensions to cell center @@ -203,8 +201,8 @@ def add_geotop_to_regis_layers( anisotropy=1.0, gt_layered=None, ): - """Combine geotop and regis in such a way that the one or more layers in - Regis are replaced by the geo_eenheden of geotop. + """Combine geotop and regis in such a way that the one or more layers in Regis are + replaced by the geo_eenheden of geotop. Parameters ---------- @@ -303,14 +301,13 @@ def add_geotop_to_regis_layers( def get_layer_names(): - """get all the available regis layer names. + """Get all the available regis layer names. Returns ------- layer_names : np.array array with names of all the regis layers. """ - layer_names = xr.open_dataset(REGIS_URL).layer.astype(str).values return layer_names diff --git a/nlmod/read/rws.py b/nlmod/read/rws.py index bc2f36e3..28b572b7 100644 --- a/nlmod/read/rws.py +++ b/nlmod/read/rws.py @@ -15,8 +15,8 @@ def get_gdf_surface_water(ds): - """read a shapefile with surface water as a geodataframe, cut by the extent - of the model. + """Read a shapefile with surface water as a geodataframe, cut by the extent of the + model. Parameters ---------- @@ -39,7 +39,7 @@ def get_gdf_surface_water(ds): @cache.cache_netcdf(coords_3d=True) def get_surface_water(ds, da_basename): - """create 3 data-arrays from the shapefile with surface water: + """Create 3 data-arrays from the shapefile with surface water: - area: area of the shape in the cell - cond: conductance based on the area and "bweerstand" column in shapefile @@ -58,7 +58,6 @@ def get_surface_water(ds, da_basename): ds : xarray.Dataset dataset with modelgrid data. """ - modelgrid = dims.modelgrid_from_ds(ds) gdf = get_gdf_surface_water(ds) @@ -93,8 +92,8 @@ def get_surface_water(ds, da_basename): @cache.cache_netcdf(coords_2d=True) def get_northsea(ds, da_name="northsea"): - """Get Dataset which is 1 at the northsea and 0 everywhere else. Sea is - defined by rws surface water shapefile. + """Get Dataset which is 1 at the northsea and 0 everywhere else. Sea is defined by + rws surface water shapefile. Parameters ---------- @@ -109,7 +108,6 @@ def get_northsea(ds, da_name="northsea"): Dataset with a single DataArray, this DataArray is 1 at sea and 0 everywhere else. Grid dimensions according to ds. """ - gdf_surf_water = get_gdf_surface_water(ds) # find grid cells with sea @@ -140,7 +138,6 @@ def add_northsea(ds, cachedir=None): b) fill top, bot, kh and kv add northsea cell by extrapolation c) get bathymetry (northsea depth) from jarkus. """ - logger.info( "Filling NaN values in top/botm and kh/kv in " "North Sea using bathymetry data from jarkus" @@ -181,8 +178,7 @@ def calculate_sea_coverage( nodata=-1, return_filled_dtm=False, ): - """ - Determine where the sea is by interpreting the digital terrain model. + """Determine where the sea is by interpreting the digital terrain model. This method assumes the pixel defined in xy_sea (by default top-left) of the DTM-DataArray is sea. It then determines the height of the sea that is required for @@ -190,7 +186,6 @@ def calculate_sea_coverage( Parameters ---------- - dtm : xr.DataArray The digital terrain data, which can be of higher resolution than ds, Nans are filled by the minial value of dtm. @@ -223,7 +218,6 @@ def calculate_sea_coverage( sea : xr.DataArray A DataArray with value of 1 where the sea is and 0 where it is not. """ - from skimage.morphology import reconstruction if not (dtm < zmax).any(): diff --git a/nlmod/read/waterboard.py b/nlmod/read/waterboard.py index 64de1fda..64eaa5a8 100644 --- a/nlmod/read/waterboard.py +++ b/nlmod/read/waterboard.py @@ -511,7 +511,6 @@ def get_data(wb, data_kind, extent=None, max_record_count=None, config=None, **k Raises ------ - DESCRIPTION. Returns @@ -594,11 +593,12 @@ def get_data(wb, data_kind, extent=None, max_record_count=None, config=None, **k def _set_column_from_columns(gdf, set_column, from_columns, nan_values=None): - """Retrieve values from one or more Geo)DataFrame-columns and set these - values as another column.""" + """Retrieve values from one or more Geo)DataFrame-columns and set these values as + another column. + """ if set_column in gdf.columns: raise (Exception(f"Column {set_column} allready exists")) - gdf[set_column] = np.NaN + gdf[set_column] = np.nan if from_columns is None: return gdf if isinstance(from_columns, str): @@ -634,5 +634,5 @@ def _set_column_from_columns(gdf, set_column, from_columns, nan_values=None): if nan_values is not None: if isinstance(nan_values, (float, int)): nan_values = [nan_values] - gdf.loc[gdf[set_column].isin(nan_values), set_column] = np.NaN + gdf.loc[gdf[set_column].isin(nan_values), set_column] = np.nan return gdf diff --git a/nlmod/read/webservices.py b/nlmod/read/webservices.py index 2e018824..97cc90bf 100644 --- a/nlmod/read/webservices.py +++ b/nlmod/read/webservices.py @@ -183,7 +183,7 @@ def arcrest( def _get_data(url, params, timeout=120, **kwargs): - """get data using a request + """Get data using a request. Parameters ---------- @@ -197,7 +197,6 @@ def _get_data(url, params, timeout=120, **kwargs): Returns ------- data - """ r = requests.get(url, params=params, timeout=timeout, **kwargs) if not r.ok: @@ -432,9 +431,8 @@ def _split_wcs_extent( fmt, crs, ): - """There is a max height and width limit for the wcs server. This function - splits your extent in chunks smaller than the limit. It returns a list of - Memory files. + """There is a max height and width limit for the wcs server. This function splits + your extent in chunks smaller than the limit. It returns a list of Memory files. Parameters ---------- @@ -463,12 +461,12 @@ def _split_wcs_extent( ------- MemoryFile Rasterio MemoryFile of the merged data + Notes ----- 1. The resolution is used to obtain the data from the wcs server. Not sure what kind of interpolation is used to resample the original grid. """ - # write tiles datasets = [] start_x = extent[0] diff --git a/nlmod/sim/__init__.py b/nlmod/sim/__init__.py index 1ca20b53..57b71d59 100644 --- a/nlmod/sim/__init__.py +++ b/nlmod/sim/__init__.py @@ -1 +1,2 @@ +# ruff: noqa: F403 from .sim import * diff --git a/nlmod/sim/sim.py b/nlmod/sim/sim.py index 052d2d9c..c33a3c64 100644 --- a/nlmod/sim/sim.py +++ b/nlmod/sim/sim.py @@ -13,9 +13,9 @@ def write_and_run(sim, ds, write_ds=True, script_path=None, silent=False): - """write modflow files and run the model. Extra options include writing the - model dataset to a netcdf file in the model workspace and copying the - modelscript to the model workspace. + """Write modflow files and run the model. Extra options include writing the model + dataset to a netcdf file in the model workspace and copying the modelscript to the + model workspace. Parameters ---------- @@ -108,7 +108,7 @@ def get_tdis_perioddata(ds, nstp="nstp", tsmult="tsmult"): def sim(ds, exe_name=None, version_tag=None): - """create sim from the model dataset. + """Create sim from the model dataset. Parameters ---------- @@ -131,7 +131,6 @@ def sim(ds, exe_name=None, version_tag=None): sim : flopy MFSimulation simulation object. """ - # start creating model logger.info("creating mf6 SIM") @@ -161,7 +160,7 @@ def sim(ds, exe_name=None, version_tag=None): def tdis(ds, sim, pname="tdis", nstp="nstp", tsmult="tsmult", **kwargs): - """create tdis package from the model dataset. + """Create tdis package from the model dataset. Parameters ---------- @@ -180,7 +179,6 @@ def tdis(ds, sim, pname="tdis", nstp="nstp", tsmult="tsmult", **kwargs): dis : flopy TDis tdis object. """ - # start creating model logger.info("creating mf6 TDIS") @@ -201,7 +199,7 @@ def tdis(ds, sim, pname="tdis", nstp="nstp", tsmult="tsmult", **kwargs): def ims(sim, complexity="MODERATE", pname="ims", **kwargs): - """create IMS package. + """Create IMS package. Parameters ---------- @@ -217,7 +215,6 @@ def ims(sim, complexity="MODERATE", pname="ims", **kwargs): ims : flopy ModflowIms ims object. """ - logger.info("creating mf6 IMS") print_option = kwargs.pop("print_option", "summary") diff --git a/nlmod/util.py b/nlmod/util.py index 0ea38fa8..e2f653cc 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -3,16 +3,16 @@ import os import re import sys -from pathlib import Path import warnings +from pathlib import Path from typing import Dict, Optional -from flopy.utils import get_modflow -from flopy.utils.get_modflow import flopy_appdata_path, get_release import geopandas as gpd import requests import xarray as xr from colorama import Back, Fore, Style +from flopy.utils import get_modflow +from flopy.utils.get_modflow import flopy_appdata_path, get_release from shapely.geometry import box logger = logging.getLogger(__name__) @@ -125,16 +125,16 @@ def get_exe_path( download_if_not_found : bool, optional Download the executables if they are not found, by default True. repo : str, default "executables" - Name of GitHub repository. Choose one of "executables" (default), - "modflow6", or "modflow6-nightly-build". If repo and version_tag are - provided the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. If not found, the executables are downloaded + Name of GitHub repository. Choose one of "executables" (default), "modflow6", + or "modflow6-nightly-build". If repo and version_tag are provided the most + recent installation location of MODFLOW is found in flopy metadata that + respects `version_tag` and `repo`. If not found, the executables are downloaded using repo and version_tag. version_tag : str, default None GitHub release ID: for example "18.0" or "latest". If repo and version_tag are - provided the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. If not found, the executables are downloaded - using repo and version_tag. + provided the most recent installation location of MODFLOW is found in flopy + metadata that respects `version_tag` and `repo`. If not found, the executables + are downloaded using repo and version_tag. Returns ------- @@ -181,8 +181,7 @@ def get_bin_directory( version_tag=None, repo="executables", ) -> Path: - """ - Get the directory where the executables are stored. + """Get the directory where the executables are stored. Searching for the executables is done in the following order: 0. If exe_name is a full path, return the full path of the executable. @@ -195,8 +194,8 @@ def get_bin_directory( Else: 4. Download the executables using `version_tag` and `repo`. - The returned directory is checked to contain exe_name if exe_name is provided. If exe_name - is set to None only the existence of the directory is checked. + The returned directory is checked to contain exe_name if exe_name is provided. If + exe_name is set to None only the existence of the directory is checked. Parameters ---------- @@ -207,17 +206,16 @@ def get_bin_directory( download_if_not_found : bool, optional Download the executables if they are not found, by default True. repo : str, default "executables" - Name of GitHub repository. Choose one of "executables" (default), - "modflow6", or "modflow6-nightly-build". If repo and version_tag are - provided the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. If not found, the executables are downloaded - using repo and version_tag. repo cannot be None. + Name of GitHub repository. Choose one of "executables" (default), "modflow6", + or "modflow6-nightly-build". If repo and version_tag are provided the most + recent installation location of MODFLOW is found in flopy metadata that + respects `version_tag` and `repo`. If not found, the executables are downloaded + using repo and version_tag. version_tag : str, default None GitHub release ID: for example "18.0" or "latest". If repo and version_tag are - provided the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. If not found, the executables are downloaded - using repo and version_tag. If version_tag is None, no version check is performed - on present executables and if no exe is found, the latest version is downloaded. + provided the most recent installation location of MODFLOW is found in flopy + metadata that respects `version_tag` and `repo`. If not found, the executables + are downloaded using repo and version_tag. Returns ------- @@ -248,7 +246,10 @@ def get_bin_directory( # If bindir is provided if bindir is not None and enable_version_check: - msg = "Incompatible arguments. If bindir is provided, unable to check the version." + msg = ( + "Incompatible arguments. If bindir is provided, " + "unable to check the version." + ) raise ValueError(msg) use_bindir = ( @@ -287,7 +288,7 @@ def get_bin_directory( download_mfbinaries( bindir=bindir, version_tag=version_tag if version_tag is not None else "latest", - repo=repo + repo=repo, ) # Rerun this function @@ -300,7 +301,10 @@ def get_bin_directory( ) else: - msg = f"Could not find {exe_name} in {bindir}, {nlmod_bindir} and {flopy_bindirs}." + msg = ( + f"Could not find {exe_name} in {bindir}, " + f"{nlmod_bindir} and {flopy_bindirs}." + ) raise FileNotFoundError(msg) @@ -315,15 +319,15 @@ def get_flopy_bin_directories(version_tag=None, repo="executables"): ---------- repo : str, default "executables" Name of GitHub repository. Choose one of "executables" (default), - "modflow6", or "modflow6-nightly-build". If repo and version_tag are - provided the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. If not found, the executables are downloaded - using repo and version_tag. + "modflow6", or "modflow6-nightly-build". If repo and version_tag are provided + the most recent installation location of MODFLOW is found in flopy metadata + that respects `version_tag` and `repo`. If not found, the executables are + downloaded using repo and version_tag. version_tag : str, default None GitHub release ID: for example "18.0" or "latest". If repo and version_tag are - provided the most recent installation location of MODFLOW is found in flopy metadata - that respects `version_tag` and `repo`. If not found, the executables are downloaded - using repo and version_tag. + provided the most recent installation location of MODFLOW is found in flopy + metadata that respects `version_tag` and `repo`. If not found, the executables + are downloaded using repo and version_tag. Returns ------- @@ -398,7 +402,6 @@ def download_mfbinaries(bindir=None, version_tag="latest", repo="executables"): "modflow6", or "modflow6-nightly-build". version_tag : str, default "latest" GitHub release ID. - """ if bindir is None: # Path objects are immutable so a copy is implied @@ -497,8 +500,10 @@ def get_da_from_da_ds(da_ds, dims=("y", "x"), data=None): def find_most_recent_file(folder, name, extension=".pklz"): - """Find the most recent file in a folder. File must startwith name and end width - extension. If you want to look for the most recent folder use extension = ''. + """Find the most recent file in a folder. + + File must startwith name and end width extension. If you want to look for the most + recent folder use extension = ''. Parameters ---------- @@ -514,7 +519,6 @@ def find_most_recent_file(folder, name, extension=".pklz"): newest_file : str name of the most recent file """ - i = 0 for file in os.listdir(folder): if file.startswith(name) and file.endswith(extension): @@ -551,7 +555,6 @@ def compare_model_extents(extent1, extent2): 1: extent1 is completely within extent2 2: extent2 is completely within extent1 """ - # option1 extent1 is completely within extent2 check_xmin = extent1[0] >= extent2[0] check_xmax = extent1[1] <= extent2[1] @@ -602,7 +605,6 @@ def polygon_from_extent(extent): polygon_ext : shapely.geometry.polygon.Polygon polygon of the extent. """ - bbox = (extent[0], extent[2], extent[1], extent[3]) polygon_ext = box(*tuple(bbox)) @@ -625,7 +627,6 @@ def gdf_from_extent(extent, crs="EPSG:28992"): gdf_extent : GeoDataFrame geodataframe with extent. """ - geom_extent = polygon_from_extent(extent) gdf_extent = gpd.GeoDataFrame(geometry=[geom_extent], crs=crs) @@ -633,8 +634,9 @@ def gdf_from_extent(extent, crs="EPSG:28992"): def gdf_within_extent(gdf, extent): - """Select only parts of the geodataframe within the extent. Only accepts Polygon and - Linestring geometry types. + """Select only parts of the geodataframe within the extent. + + Only accepts Polygon and Linestring geometry types. Parameters ---------- @@ -688,6 +690,7 @@ def get_google_drive_filename(fid, timeout=120): warnings.warn( "this function is no longer supported use the gdown package instead", DeprecationWarning, + stacklevel=1, ) if isinstance(id, requests.Response): @@ -714,6 +717,7 @@ def download_file_from_google_drive(fid, destination=None): warnings.warn( "this function is no longer supported use the gdown package instead", DeprecationWarning, + stacklevel=1, ) def get_confirm_token(response): @@ -805,14 +809,12 @@ def __init__( self, *args, colors: Optional[Dict[str, str]] = None, **kwargs ) -> None: """Initialize the formatter with specified format strings.""" - super().__init__(*args, **kwargs) self.colors = colors if colors else {} def format(self, record) -> str: """Format the specified record as text.""" - record.color = self.colors.get(record.levelname, "") record.reset = Style.RESET_ALL @@ -820,6 +822,18 @@ def format(self, record) -> str: def get_color_logger(level="INFO"): + """Get a logger with colored output. + + Parameters + ---------- + level : str, optional + The logging level to set for the logger. Default is "INFO". + + Returns + ------- + logger : logging.Logger + The configured logger object. + """ if level == "DEBUG": FORMAT = "{color}{levelname}:{name}.{funcName}:{lineno}:{message}{reset}" else: diff --git a/nlmod/version.py b/nlmod/version.py index 5e1c0340..e281423c 100644 --- a/nlmod/version.py +++ b/nlmod/version.py @@ -6,15 +6,12 @@ def show_versions() -> None: """Method to print the version of dependencies.""" - msg = ( - f"Python version: {python_version()}\n" - f"NumPy version: {metadata.version('numpy')}\n" - f"Xarray version: {metadata.version('xarray')}\n" - f"Matplotlib version: {metadata.version('matplotlib')}\n" - f"Flopy version: {metadata.version('flopy')}\n" + f"Python version : {python_version()}\n" + f"NumPy version : {metadata.version('numpy')}\n" + f"Xarray version : {metadata.version('xarray')}\n" + f"Matplotlib version : {metadata.version('matplotlib')}\n" + f"Flopy version : {metadata.version('flopy')}\n\n" + f"nlmod version : {__version__}" ) - - msg += f"\nnlmod version: {__version__}" - - return print(msg) + print(msg) diff --git a/pyproject.toml b/pyproject.toml index e01548c2..4867067e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,13 +57,7 @@ repository = "https://github.com/gwmod/nlmod" documentation = "https://nlmod.readthedocs.io/en/latest/" [project.optional-dependencies] -full = [ - "nlmod[knmi]", - "gdown", - "geocube", - "contextily", - "scikit-image", -] +full = ["nlmod[knmi]", "gdown", "geocube", "contextily", "scikit-image"] knmi = ["h5netcdf", "nlmod[grib]"] grib = ["cfgrib", "ecmwflibs"] test = ["pytest>=7", "pytest-cov", "pytest-dependency"] @@ -103,9 +97,32 @@ line-length = 88 profile = "black" [tool.ruff] -line-length = 88 +line-length = 88 extend-include = ["*.ipynb"] +[tool.ruff.lint] +# See: https://docs.astral.sh/ruff/rules/ +select = [ + "C4", # flake8-comprehensions + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "PT", # pytest-style + "D", # pydocstyle + "B", # flake8-bugbear + "NPY", # numpy +] +ignore = [ + "D401", # Imperative mood for docstring. Be glad we have docstrings at all :P! + "D100", # Missing docstring in module. + "D104", # Missing docstring in public package. +] + +[tool.ruff.format] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + [tool.pytest.ini_options] addopts = "--strict-markers --durations=0 --cov-report xml:coverage.xml --cov nlmod -v" markers = ["notebooks: run notebooks", "slow: slow tests", "skip: skip tests"] diff --git a/tests/test_001_model.py b/tests/test_001_model.py index 13e2a66e..3f0e4c1d 100644 --- a/tests/test_001_model.py +++ b/tests/test_001_model.py @@ -75,7 +75,7 @@ def test_get_ds_variable_delrc(): ) -@pytest.mark.slow +@pytest.mark.slow() def test_create_small_model_grid_only(tmpdir, model_name="test"): extent = [98700.0, 99000.0, 489500.0, 489700.0] # extent, nrow, ncol = nlmod.read.regis.fit_extent_to_regis(extent, 100, 100) @@ -117,7 +117,7 @@ def test_create_small_model_grid_only(tmpdir, model_name="test"): ds.to_netcdf(os.path.join(tst_model_dir, "small_model.nc")) -@pytest.mark.slow +@pytest.mark.slow() def test_create_sea_model_grid_only(tmpdir, model_name="test"): extent = [95000.0, 105000.0, 494000.0, 500000.0] # extent, nrow, ncol = nlmod.read.regis.fit_extent_to_regis(extent, 100, 100) @@ -143,7 +143,7 @@ def test_create_sea_model_grid_only(tmpdir, model_name="test"): ds.to_netcdf(os.path.join(tst_model_dir, "basic_sea_model.nc")) -@pytest.mark.slow +@pytest.mark.slow() def test_create_sea_model_grid_only_delr_delc_50(tmpdir, model_name="test"): ds = get_ds_time_transient(tmpdir) extent = [95000.0, 105000.0, 494000.0, 500000.0] @@ -160,7 +160,7 @@ def test_create_sea_model_grid_only_delr_delc_50(tmpdir, model_name="test"): ds.to_netcdf(os.path.join(tst_model_dir, "sea_model_grid_50.nc")) -@pytest.mark.slow +@pytest.mark.slow() def test_create_sea_model(tmpdir): ds = xr.open_dataset( os.path.join(tst_model_dir, "basic_sea_model.nc"), mask_and_scale=False @@ -210,7 +210,7 @@ def test_create_sea_model(tmpdir): _ = nlmod.sim.write_and_run(sim, ds) -@pytest.mark.slow +@pytest.mark.slow() def test_create_sea_model_perlen_list(tmpdir): ds = xr.open_dataset(os.path.join(tst_model_dir, "basic_sea_model.nc")) @@ -280,7 +280,7 @@ def test_create_sea_model_perlen_list(tmpdir): nlmod.sim.write_and_run(sim, ds) -@pytest.mark.slow +@pytest.mark.slow() def test_create_sea_model_perlen_14(tmpdir): ds = xr.open_dataset(os.path.join(tst_model_dir, "basic_sea_model.nc")) diff --git a/tests/test_002_regis_geotop.py b/tests/test_002_regis_geotop.py index 8b46145c..dced58ba 100644 --- a/tests/test_002_regis_geotop.py +++ b/tests/test_002_regis_geotop.py @@ -1,4 +1,5 @@ import matplotlib.pyplot as plt + import nlmod diff --git a/tests/test_003_mfpackages.py b/tests/test_003_mfpackages.py index 86b08b1b..6857d9d2 100644 --- a/tests/test_003_mfpackages.py +++ b/tests/test_003_mfpackages.py @@ -94,7 +94,7 @@ def get_value_from_ds_datavar(): }, ) shape = list(ds.sizes.values()) - ds["test_var"] = ("layer", "y", "x"), np.arange(np.product(shape)).reshape(shape) + ds["test_var"] = ("layer", "y", "x"), np.arange(np.prod(shape)).reshape(shape) # get value from ds v0 = nlmod.util._get_value_from_ds_datavar( diff --git a/tests/test_005_external_data.py b/tests/test_005_external_data.py index 709bdaf0..1d40d0d1 100644 --- a/tests/test_005_external_data.py +++ b/tests/test_005_external_data.py @@ -1,8 +1,8 @@ import pandas as pd -import xarray as xr -from shapely.geometry import LineString import pytest import test_001_model +import xarray as xr +from shapely.geometry import LineString import nlmod diff --git a/tests/test_007_run_notebooks.py b/tests/test_007_run_notebooks.py index 1ed78c4d..8b27e8b8 100644 --- a/tests/test_007_run_notebooks.py +++ b/tests/test_007_run_notebooks.py @@ -1,4 +1,5 @@ """run notebooks in the examples directory.""" +# ruff: noqa: D103 import os import nbformat @@ -19,91 +20,91 @@ def _run_notebook(nbdir, fname): return out -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_00_model_from_scratch(): _run_notebook(nbdir, "00_model_from_scratch.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_01_basic_model(): _run_notebook(nbdir, "01_basic_model.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_02_surface_water(): _run_notebook(nbdir, "02_surface_water.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_03_local_grid_refinement(): _run_notebook(nbdir, "03_local_grid_refinement.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_04_modifying_layermodels(): _run_notebook(nbdir, "04_modifying_layermodels.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_05_caching(): _run_notebook(nbdir, "05_caching.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_06_gridding_vector_data(): _run_notebook(nbdir, "06_gridding_vector_data.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_07_resampling(): _run_notebook(nbdir, "07_resampling.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_08_gis(): _run_notebook(nbdir, "08_gis.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_09_schoonhoven(): _run_notebook(nbdir, "09_schoonhoven.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_10_modpath(): _run_notebook(nbdir, "10_modpath.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_11_grid_rotation(): _run_notebook(nbdir, "11_grid_rotation.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_12_layer_generation(): _run_notebook(nbdir, "12_layer_generation.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_13_plot_methods(): _run_notebook(nbdir, "13_plot_methods.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_14_stromingen_example(): _run_notebook(nbdir, "14_stromingen_example.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_15_geotop(): _run_notebook(nbdir, "15_geotop.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_16_groundwater_transport(): _run_notebook(nbdir, "16_groundwater_transport.ipynb") -@pytest.mark.notebooks +@pytest.mark.notebooks() def test_run_notebook_17_unsaturated_zone_flow(): _run_notebook(nbdir, "17_unsaturated_zone_flow.ipynb") diff --git a/tests/test_008_waterschappen.py b/tests/test_008_waterschappen.py index 6b81bdc2..e0e952f3 100644 --- a/tests/test_008_waterschappen.py +++ b/tests/test_008_waterschappen.py @@ -3,7 +3,6 @@ import nlmod - # def test_download_polygons(): # is tested in test_024_administrative.test_get_waterboards # nlmod.read.waterboard.get_polygons() diff --git a/tests/test_009_layers.py b/tests/test_009_layers.py index ce19af49..3b304872 100644 --- a/tests/test_009_layers.py +++ b/tests/test_009_layers.py @@ -1,7 +1,7 @@ import os -import numpy as np import matplotlib.pyplot as plt +import numpy as np from shapely.geometry import LineString import nlmod diff --git a/tests/test_018_knmi_data_platform.py b/tests/test_018_knmi_data_platform.py index 9c53bbb0..db64b8c0 100644 --- a/tests/test_018_knmi_data_platform.py +++ b/tests/test_018_knmi_data_platform.py @@ -1,11 +1,14 @@ +# ruff: noqa: D103 import os from pathlib import Path +import pytest + from nlmod.read import knmi_data_platform data_path = Path(__file__).parent / "data" - +@pytest.mark.skip(reason="FileNotFoundError: download/INTER_OPER_R___EV24____L3__20240626T000000_20240627T000000_0003.nc not found") def test_download_multiple_nc_files() -> None: dataset_name = "EV24" dataset_version = "2" @@ -30,7 +33,7 @@ def test_download_multiple_nc_files() -> None: # plot the mean evaporation ds["prediction"].mean("time").plot() - +@pytest.mark.skip(reason="KeyError: 'files'") def test_download_read_zip_file() -> None: dataset_name = "rad_nl25_rac_mfbs_24h_netcdf4" dataset_version = "2.0" diff --git a/tests/test_019_attributes_encodings.py b/tests/test_019_attributes_encodings.py index 8e18ce38..03333f9d 100644 --- a/tests/test_019_attributes_encodings.py +++ b/tests/test_019_attributes_encodings.py @@ -1,5 +1,4 @@ import os -import time from tempfile import TemporaryDirectory import numpy as np diff --git a/tests/test_021_nhi.py b/tests/test_021_nhi.py index 6f5e06f8..8be1c5be 100644 --- a/tests/test_021_nhi.py +++ b/tests/test_021_nhi.py @@ -1,16 +1,19 @@ +# ruff: noqa: D103 import os -import numpy as np -import geopandas as gpd import tempfile -import nlmod -import pytest + +import geopandas as gpd import matplotlib.pyplot as plt +import numpy as np +import pytest + +import nlmod tmpdir = tempfile.gettempdir() -@pytest.mark.slow -def test_buidrainage(): +@pytest.mark.slow() +def test_buisdrainage(): model_ws = os.path.join(tmpdir, "buidrain") ds = nlmod.get_ds([110_000, 130_000, 435_000, 445_000], model_ws=model_ws) ds = nlmod.read.nhi.add_buisdrainage(ds) diff --git a/tests/test_022_gwt.py b/tests/test_022_gwt.py index 3f864c4f..5d9fce1e 100644 --- a/tests/test_022_gwt.py +++ b/tests/test_022_gwt.py @@ -1,7 +1,9 @@ -import tempfile import os +import tempfile + import pandas as pd import xarray as xr + import nlmod diff --git a/tests/test_023_hfb.py b/tests/test_023_hfb.py index f7e2a73f..1902f23d 100644 --- a/tests/test_023_hfb.py +++ b/tests/test_023_hfb.py @@ -1,8 +1,10 @@ -from shapely.geometry import LineString, Polygon -import geopandas as gpd +# ruff: noqa: D103 import flopy -import nlmod +import geopandas as gpd import util +from shapely.geometry import LineString, Polygon + +import nlmod def test_get_hfb_spd(): diff --git a/tests/test_025_modpath.py b/tests/test_025_modpath.py index 6e22c1a6..e18fb5f7 100644 --- a/tests/test_025_modpath.py +++ b/tests/test_025_modpath.py @@ -1,6 +1,8 @@ import os -import xarray as xr + import flopy +import xarray as xr + import nlmod diff --git a/tests/test_026_grid.py b/tests/test_026_grid.py index b646e5a0..80b001e7 100644 --- a/tests/test_026_grid.py +++ b/tests/test_026_grid.py @@ -1,9 +1,11 @@ -import tempfile import os -import numpy as np -import xarray as xr +import tempfile + import geopandas as gpd import matplotlib.pyplot as plt +import numpy as np +import xarray as xr + import nlmod model_ws = os.path.join(tempfile.gettempdir(), "test_grid") @@ -119,13 +121,13 @@ def test_vertex_da_to_ds(): def test_fillnan_da(): # for a structured grid ds = get_structured_model_ds() - ds["top"][5, 5] = np.NaN + ds["top"][5, 5] = np.nan top = nlmod.resample.fillnan_da(ds["top"], ds=ds) assert not np.isnan(top[5, 5]) # also for a vertex grid ds = get_vertex_model_ds() - ds["top"][100] = np.NaN + ds["top"][100] = np.nan mask = ds["top"].isnull() assert mask.any() top = nlmod.resample.fillnan_da(ds["top"], ds=ds) From 295623dec4b180e36efd3edfdcf7caeff87e6ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Tue, 2 Jul 2024 14:10:40 +0200 Subject: [PATCH 61/85] allow pathlib paths in write_and_run --- nlmod/sim/sim.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nlmod/sim/sim.py b/nlmod/sim/sim.py index c33a3c64..4d5e2438 100644 --- a/nlmod/sim/sim.py +++ b/nlmod/sim/sim.py @@ -1,6 +1,7 @@ import datetime as dt import logging import os +import pathlib from shutil import copyfile import flopy @@ -51,7 +52,10 @@ def write_and_run(sim, ds, write_ds=True, script_path=None, silent=False): ds.attrs["model_dataset_written_to_disk_on"] = dt.datetime.now().strftime( "%Y%m%d_%H:%M:%S" ) - ds.to_netcdf(os.path.join(ds.attrs["model_ws"], f"{ds.model_name}.nc")) + if isinstance(ds.attrs["model_ws"], pathlib.PurePath): + ds.to_netcdf(ds.attrs["model_ws"] / f"{ds.model_name}.nc") + else: + ds.to_netcdf(os.path.join(ds.attrs["model_ws"], f"{ds.model_name}.nc")) logger.info("write modflow files to model workspace") sim.write_simulation(silent=silent) From 6e12f995e2ffa28e499c307660d99172a273a321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Tue, 2 Jul 2024 14:17:50 +0200 Subject: [PATCH 62/85] Remove circular import --- nlmod/dims/grid.py | 6 +++++- nlmod/dims/resample.py | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 50245f61..f312e48e 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -28,7 +28,6 @@ from tqdm import tqdm from .. import cache, util -from .base import _get_structured_grid_ds, _get_vertex_grid_ds, extrapolate_ds from .layers import ( fill_nan_top_botm_kh_kv, get_first_active_layer, @@ -312,6 +311,7 @@ def modelgrid_to_ds(mg): """ if mg.grid_type == "structured": x, y = mg.xyedges + from .base import _get_structured_grid_ds ds = _get_structured_grid_ds( xedges=x, @@ -326,6 +326,8 @@ def modelgrid_to_ds(mg): crs=None, ) elif mg.grid_type == "vertex": + from .base import _get_vertex_grid_ds + ds = _get_vertex_grid_ds( x=mg.xcellcenters, y=mg.ycellcenters, @@ -734,6 +736,8 @@ def update_ds_from_layer_ds(ds, layer_ds, method="nearest", **kwargs): for var in layer_ds.data_vars: ds[var] = structured_da_to_ds(layer_ds[var], ds, method=method) + from .base import extrapolate_ds + ds = extrapolate_ds(ds) ds = fill_nan_top_botm_kh_kv(ds, **kwargs) return ds diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index ff88491a..7311a6a0 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -6,11 +6,8 @@ import xarray as xr from scipy.interpolate import griddata from scipy.spatial import cKDTree - - from ..util import get_da_from_da_ds - logger = logging.getLogger(__name__) From a04b0c65a51672416439de6cbe8f7e5f0e4f648c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Tue, 2 Jul 2024 15:35:26 +0200 Subject: [PATCH 63/85] sort imports --- nlmod/dims/base.py | 2 +- nlmod/dims/grid.py | 2 +- nlmod/dims/rdp.py | 6 +++--- nlmod/dims/resample.py | 1 + nlmod/gis.py | 2 +- nlmod/gwf/gwf.py | 2 +- nlmod/gwf/output.py | 2 +- nlmod/gwf/surface_water.py | 6 +++--- nlmod/mfoutput/mfoutput.py | 2 +- nlmod/plot/dcs.py | 2 +- nlmod/plot/plot.py | 2 +- nlmod/read/knmi.py | 2 +- nlmod/util.py | 4 +--- 13 files changed, 17 insertions(+), 18 deletions(-) diff --git a/nlmod/dims/base.py b/nlmod/dims/base.py index c1d3279d..5c32aacd 100644 --- a/nlmod/dims/base.py +++ b/nlmod/dims/base.py @@ -8,7 +8,7 @@ from .. import util from ..epsg28992 import EPSG_28992 -from . import resample, grid +from . import grid, resample from .layers import fill_nan_top_botm_kh_kv logger = logging.getLogger(__name__) diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 74da2dfa..4e762276 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -16,13 +16,13 @@ import pandas as pd import shapely import xarray as xr +from affine import Affine from flopy.discretization.structuredgrid import StructuredGrid from flopy.discretization.vertexgrid import VertexGrid from flopy.utils.gridgen import Gridgen from flopy.utils.gridintersect import GridIntersect from packaging import version from scipy.interpolate import griddata -from affine import Affine from shapely.affinity import affine_transform from shapely.geometry import Point, Polygon from tqdm import tqdm diff --git a/nlmod/dims/rdp.py b/nlmod/dims/rdp.py index df996789..bd688d38 100644 --- a/nlmod/dims/rdp.py +++ b/nlmod/dims/rdp.py @@ -1,8 +1,8 @@ """ -rdp -~~~ +rdp +~~~ Python implementation of the Ramer-Douglas-Peucker algorithm. -:copyright: 2014-2016 Fabian Hirschmann +:copyright: 2014-2016 Fabian Hirschmann :license: MIT, see LICENSE.txt for more details. """ diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index ac8acaee..dcd5ed01 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -6,6 +6,7 @@ import xarray as xr from scipy.interpolate import griddata from scipy.spatial import cKDTree + from ..util import get_da_from_da_ds logger = logging.getLogger(__name__) diff --git a/nlmod/gis.py b/nlmod/gis.py index d70d5e17..3ef06361 100644 --- a/nlmod/gis.py +++ b/nlmod/gis.py @@ -4,7 +4,7 @@ import geopandas as gpd import numpy as np -from .dims.grid import polygons_from_model_ds, get_affine_mod_to_world +from .dims.grid import get_affine_mod_to_world, polygons_from_model_ds from .dims.layers import calculate_thickness logger = logging.getLogger(__name__) diff --git a/nlmod/gwf/gwf.py b/nlmod/gwf/gwf.py index 1471a768..2afc8586 100644 --- a/nlmod/gwf/gwf.py +++ b/nlmod/gwf/gwf.py @@ -6,7 +6,7 @@ import xarray as xr from ..dims import grid -from ..dims.grid import get_delr, get_delc +from ..dims.grid import get_delc, get_delr from ..dims.layers import get_idomain from ..sim import ims, sim, tdis from ..util import _get_value_from_ds_attr, _get_value_from_ds_datavar diff --git a/nlmod/gwf/output.py b/nlmod/gwf/output.py index e4fd590c..ac32f29e 100644 --- a/nlmod/gwf/output.py +++ b/nlmod/gwf/output.py @@ -7,7 +7,7 @@ import xarray as xr from shapely.geometry import Point -from ..dims.grid import modelgrid_from_ds, get_affine_world_to_mod +from ..dims.grid import get_affine_world_to_mod, modelgrid_from_ds from ..mfoutput.mfoutput import ( _get_budget_da, _get_flopy_data_object, diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index c3e6e7dc..b912490e 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -11,15 +11,15 @@ from tqdm import tqdm from ..cache import cache_pickle -from ..util import extent_to_polygon from ..dims.grid import ( gdf_to_grid, - get_extent_polygon, - get_delr, get_delc, + get_delr, + get_extent_polygon, ) from ..dims.layers import get_idomain from ..read import bgt, waterboard +from ..util import extent_to_polygon logger = logging.getLogger(__name__) diff --git a/nlmod/mfoutput/mfoutput.py b/nlmod/mfoutput/mfoutput.py index d6600607..f1d06eb9 100644 --- a/nlmod/mfoutput/mfoutput.py +++ b/nlmod/mfoutput/mfoutput.py @@ -7,9 +7,9 @@ import xarray as xr from ..dims.grid import ( + get_affine_mod_to_world, get_dims_coords_from_modelgrid, modelgrid_from_ds, - get_affine_mod_to_world, ) from ..dims.time import ds_time_idx from .binaryfile import _get_binary_budget_data, _get_binary_head_data diff --git a/nlmod/plot/dcs.py b/nlmod/plot/dcs.py index 9430e90e..0a879445 100644 --- a/nlmod/plot/dcs.py +++ b/nlmod/plot/dcs.py @@ -14,7 +14,7 @@ from shapely.affinity import affine_transform from shapely.geometry import LineString, MultiLineString, Point, Polygon -from ..dims.grid import modelgrid_from_ds, get_affine_world_to_mod +from ..dims.grid import get_affine_world_to_mod, modelgrid_from_ds from .plotutil import get_map logger = logging.getLogger(__name__) diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index dc78bcc0..fd3c39f9 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -13,10 +13,10 @@ from mpl_toolkits.axes_grid1 import make_axes_locatable from ..dims.grid import ( - modelgrid_from_ds, get_affine_mod_to_world, get_extent, get_extent_gdf, + modelgrid_from_ds, ) from ..read import geotop, rws from .dcs import DatasetCrossSection diff --git a/nlmod/read/knmi.py b/nlmod/read/knmi.py index 86455489..9344324b 100644 --- a/nlmod/read/knmi.py +++ b/nlmod/read/knmi.py @@ -7,8 +7,8 @@ from hydropandas.io import knmi as hpd_knmi from .. import cache, util -from ..dims.layers import get_first_active_layer from ..dims.grid import get_affine_mod_to_world +from ..dims.layers import get_first_active_layer logger = logging.getLogger(__name__) diff --git a/nlmod/util.py b/nlmod/util.py index ac6ba69b..ed052967 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -13,7 +13,7 @@ from colorama import Back, Fore, Style from flopy.utils import get_modflow from flopy.utils.get_modflow import flopy_appdata_path, get_release -from shapely.geometry import box, Polygon +from shapely.geometry import Polygon, box logger = logging.getLogger(__name__) @@ -595,7 +595,6 @@ def compare_model_extents(extent1, extent2): def extent_to_polygon(extent): """Generate a shapely Polygon from an extent ([xmin, xmax, ymin, ymax]) - Parameters ---------- extent : tuple, list or array @@ -630,7 +629,6 @@ def extent_to_gdf(extent, crs="EPSG:28992"): gdf_extent : geopandas.GeoDataFrame geodataframe with extent. """ - geom_extent = extent_to_polygon(extent) gdf_extent = gpd.GeoDataFrame(geometry=[geom_extent], crs=crs) From d2910f39de03411686913c60bccc134a736d4beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Tue, 2 Jul 2024 16:24:58 +0200 Subject: [PATCH 64/85] codacy stuff and import order of dims --- nlmod/dims/__init__.py | 4 ++-- nlmod/dims/grid.py | 1 + nlmod/dims/resample.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nlmod/dims/__init__.py b/nlmod/dims/__init__.py index 18c147c6..14c2d185 100644 --- a/nlmod/dims/__init__.py +++ b/nlmod/dims/__init__.py @@ -2,7 +2,7 @@ from . import base, grid, layers, resample, time from .attributes_encodings import * from .base import * -from .grid import * -from .layers import * from .resample import * +from .grid import * # import from grid after resample, to ignore deprecated methods +from .layers import * from .time import * diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 4e762276..440df8d9 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -6,6 +6,7 @@ - fill, interpolate and resample grid data """ +# ruff: noqa: E402 import logging import os import warnings diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index dcd5ed01..8952f496 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -1,3 +1,4 @@ +# ruff: noqa: E402 import logging import numbers From f46101c2f9c4a31fb458e553b8e550a70c08c442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Tue, 2 Jul 2024 16:37:47 +0200 Subject: [PATCH 65/85] Small improvements to notebooks --- docs/examples/01_basic_model.ipynb | 1 - docs/examples/04_modifying_layermodels.ipynb | 4 ++-- docs/examples/11_grid_rotation.ipynb | 6 +++--- nlmod/dims/grid.py | 1 - nlmod/dims/resample.py | 1 - 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/examples/01_basic_model.ipynb b/docs/examples/01_basic_model.ipynb index ed358fb3..a8bd308e 100644 --- a/docs/examples/01_basic_model.ipynb +++ b/docs/examples/01_basic_model.ipynb @@ -18,7 +18,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "import nlmod" ] }, diff --git a/docs/examples/04_modifying_layermodels.ipynb b/docs/examples/04_modifying_layermodels.ipynb index f7fe8687..f8ef9abd 100644 --- a/docs/examples/04_modifying_layermodels.ipynb +++ b/docs/examples/04_modifying_layermodels.ipynb @@ -455,7 +455,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Set mimimum layer thickness\n", + "## Set minimum layer thickness\n", "nlmod.layers.set_minimum layer_thickness increases the thickness of a layer if the thickness is less than a specified value. With a parameter called 'change' you can specify in which direction the layer is changed. The only supported option for now is 'botm', which changes the layer botm. \n", "\n", "This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before." @@ -467,7 +467,7 @@ "metadata": {}, "outputs": [], "source": [ - "# set the mimimum thickness of 'PZWAz2' to 20 m\n", + "# set the minimum thickness of 'PZWAz2' to 20 m\n", "ds_new = nlmod.layers.set_minimum_layer_thickness(ds.copy(deep=True), \"PZWAz2\", 20.0)\n", "compare_layer_models(ds, line, colors, ds2=ds_new, title2=\"Modified\")" ] diff --git a/docs/examples/11_grid_rotation.ipynb b/docs/examples/11_grid_rotation.ipynb index 61f3d8d0..8b2dc97f 100644 --- a/docs/examples/11_grid_rotation.ipynb +++ b/docs/examples/11_grid_rotation.ipynb @@ -131,7 +131,7 @@ "outputs": [], "source": [ "# Download AHN\n", - "extent = nlmod.resample.get_extent(ds)\n", + "extent = nlmod.grid.get_extent(ds)\n", "ahn = nlmod.read.ahn.get_ahn3(extent)\n", "\n", "# Resample to the grid\n", @@ -320,8 +320,8 @@ "cbar = nlmod.plot.colorbar_inside(pc)\n", "# as the surface water shapes are in model coordinates, we need to transform them\n", "# to real-world coordinates before plotting\n", - "affine = nlmod.resample.get_affine_mod_to_world(ds)\n", - "bgt_rw = nlmod.resample.affine_transform_gdf(bgt, affine)\n", + "affine = nlmod.grid.get_affine_mod_to_world(ds)\n", + "bgt_rw = nlmod.grid.affine_transform_gdf(bgt, affine)\n", "bgt_rw.plot(ax=ax, edgecolor=\"k\", facecolor=\"none\")" ] }, diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 440df8d9..4e762276 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -6,7 +6,6 @@ - fill, interpolate and resample grid data """ -# ruff: noqa: E402 import logging import os import warnings diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index 8952f496..dcd5ed01 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -1,4 +1,3 @@ -# ruff: noqa: E402 import logging import numbers From 2c228b689f9da6b56ea644e4778c9bb7bdae1429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Tue, 2 Jul 2024 17:06:31 +0200 Subject: [PATCH 66/85] Add codacy rules and replace one use of polygon_from_extent --- .prospector.yaml | 2 ++ nlmod/dims/grid.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.prospector.yaml b/.prospector.yaml index 57c8ec7c..4656c947 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -29,6 +29,8 @@ pylint: - too-many-branches - too-many-statements - logging-fstring-interpolation + - import-outside-toplevel + - implicit-str-concat mccabe: disable: diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py index 4e762276..2b8a3625 100644 --- a/nlmod/dims/grid.py +++ b/nlmod/dims/grid.py @@ -1956,7 +1956,7 @@ def mask_model_edge(ds, idomain=None): ds["vertices"] = get_vertices(ds) polygons_grid = polygons_from_model_ds(ds) gdf_grid = gpd.GeoDataFrame(geometry=polygons_grid) - extent_edge = util.polygon_from_extent(ds.extent).exterior + extent_edge = get_extent_polygon(ds).exterior cids_edge = gdf_grid.loc[gdf_grid.touches(extent_edge)].index ds_out["edge_mask"] = util.get_da_from_da_ds( ds, dims=("layer", "icell2d"), data=0 From 0224b305187f317d108cdad449c44944d2fb061b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Tue, 2 Jul 2024 22:26:37 +0200 Subject: [PATCH 67/85] Update resample.py --- nlmod/dims/resample.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nlmod/dims/resample.py b/nlmod/dims/resample.py index dcd5ed01..dc5594a5 100644 --- a/nlmod/dims/resample.py +++ b/nlmod/dims/resample.py @@ -615,7 +615,7 @@ def affine_transform_gdf(gdf, affine): def get_extent(ds, rotated=True): """Get the model extent, corrected for angrot if necessary.""" logger.warning( - "nlmod.resample.get_extent is deprecated. " "Use nlmod.grid.get_extent instead" + "nlmod.resample.get_extent is deprecated. Use nlmod.grid.get_extent instead" ) from .grid import get_extent @@ -647,8 +647,8 @@ def get_affine_world_to_mod(ds): def get_affine(ds, sx=None, sy=None): """Get the affine-transformation, from pixel to real-world coordinates.""" logger.warning( - "nlmod.resample.get_affine is deprecated. " "Use nlmod.grid.get_affine instead" + "nlmod.resample.get_affine is deprecated. Use nlmod.grid.get_affine instead" ) from .grid import get_affine - return get_affine(ds) + return get_affine(ds, sx=sx, sy=sy) From fe8d2cae116337f7655880a9763e5ee2e91fac31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Wed, 3 Jul 2024 14:09:39 +0200 Subject: [PATCH 68/85] docstring comments @OnnoEbbens --- nlmod/plot/plot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index fd3c39f9..c0ab4e7d 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -59,9 +59,14 @@ def modelextent(ds, dx=None, ax=None, rotated=True, **kwargs): ---------- ds : xarray.Dataset The dataset containing the data. + dx : float, optional + The buffer around the model extent. Default is 5% of the longest model edge. ax : matplotlib.axes.Axes, optional The axes object to plot on. If not provided, a new figure and axes will be created. + rotated : bool, optional + Plot the model extent in real-world coordinates for rotated grids. Default is + True. Set to False to plot model extent in local coordinates. **kwargs Additional keyword arguments to pass to the boundary plot. From 0366f8d6a5c13c05177f3dcf6bb10e71b280d627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Wed, 3 Jul 2024 15:37:00 +0200 Subject: [PATCH 69/85] Set rotated to False in plot-methods --- nlmod/plot/plot.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/nlmod/plot/plot.py b/nlmod/plot/plot.py index c0ab4e7d..30407da2 100644 --- a/nlmod/plot/plot.py +++ b/nlmod/plot/plot.py @@ -1,3 +1,4 @@ +import logging import warnings from functools import partial @@ -28,6 +29,8 @@ title_inside, ) +logger = logging.getLogger(__name__) + def surface_water(model_ds, ax=None, **kwargs): surf_water = rws.get_gdf_surface_water(model_ds) @@ -52,7 +55,7 @@ def modelgrid(ds, ax=None, **kwargs): return ax -def modelextent(ds, dx=None, ax=None, rotated=True, **kwargs): +def modelextent(ds, dx=None, ax=None, rotated=False, **kwargs): """Plot model extent. Parameters @@ -65,8 +68,8 @@ def modelextent(ds, dx=None, ax=None, rotated=True, **kwargs): The axes object to plot on. If not provided, a new figure and axes will be created. rotated : bool, optional - Plot the model extent in real-world coordinates for rotated grids. Default is - True. Set to False to plot model extent in local coordinates. + When True, plot the model extent in real-world coordinates for rotated grids. + The default is False, which plots the model extent in local coordinates. **kwargs Additional keyword arguments to pass to the boundary plot. @@ -212,7 +215,8 @@ def data_array(da, ds=None, ax=None, rotated=False, edgecolor=None, **kwargs): ax : matplotlib.Axes, optional The axes used for plotting. Set to current axes when None. The default is None. rotated : bool, optional - Plot the data-array in rotated coordinates + When True, plot the data-array in real-world coordinates for rotated grids. + The default is False, which plots the data-array in local coordinates. **kwargs : cit Kwargs are passed to PatchCollection (vertex) or pcolormesh (structured). @@ -407,7 +411,7 @@ def _get_geotop_cmap_and_norm(lithok, lithok_props): return array, cmap, norm -def _get_figure(ax=None, da=None, ds=None, figsize=None, rotated=True, extent=None): +def _get_figure(ax=None, da=None, ds=None, figsize=None, rotated=False, extent=None): # figure if ax is not None: f = ax.figure @@ -458,7 +462,7 @@ def map_array( colorbar=True, colorbar_label="", plot_grid=True, - rotated=True, + rotated=False, add_to_plot=None, background=False, figsize=None, @@ -519,7 +523,10 @@ def map_array( # bgmap if background: - add_background_map(ax, map_provider="nlmaps.water", alpha=0.5) + if not rotated and "angrot" in ds.attrs and ds.attrs["angrot"] != 0.0: + logger.warning("Background map not supported in in model coordinates") + else: + add_background_map(ax, map_provider="nlmaps.water", alpha=0.5) # add other info to plot if add_to_plot is not None: @@ -578,7 +585,7 @@ def animate_map( colorbar=True, colorbar_label="", plot_grid=True, - rotated=True, + rotated=False, background=False, figsize=None, ax=None, @@ -625,7 +632,7 @@ def animate_map( plot_grid : bool, optional Whether to plot the model grid. Default is True. rotated : bool, optional - Whether to plot rotated model, if applicable. Default is True. + Whether to plot rotated model, if applicable. Default is False. background : bool, optional Whether to add a background map. Default is False. figsize : tuple, optional From 296cd2695df02cfc6b75e8d1f2ee193a3adbe455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Wed, 3 Jul 2024 16:53:55 +0200 Subject: [PATCH 70/85] remove deprecation message for set_ds_time --- nlmod/dims/time.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nlmod/dims/time.py b/nlmod/dims/time.py index 0af4d927..8624aab9 100644 --- a/nlmod/dims/time.py +++ b/nlmod/dims/time.py @@ -200,11 +200,6 @@ def set_ds_time( ds : xarray.Dataset model dataset with added time coordinate """ - logger.info( - "Function set_ds_time() has changed since nlmod version 0.7." - " For the old behavior, use `nlmod.time.set_ds_time_deprecated()`." - ) - if time is None and perlen is None: raise (ValueError("Please specify either time or perlen in set_ds_time")) elif perlen is not None: From 7b372d628729b30a27f9c69d0a19ac06b809601c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Wed, 3 Jul 2024 16:55:14 +0200 Subject: [PATCH 71/85] add ds_time_to_pandas_index - convert ds time coordinate to pandas DateTimeIndex, optionally with the starting date. - can be useful for resampling time series to model period --- nlmod/dims/time.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/nlmod/dims/time.py b/nlmod/dims/time.py index 8624aab9..9f481d63 100644 --- a/nlmod/dims/time.py +++ b/nlmod/dims/time.py @@ -542,3 +542,24 @@ def dataframe_to_flopy_timeseries( time_series_namerecord=time_series_namerecord, interpolation_methodrecord=interpolation_methodrecord, ) + + +def ds_time_to_pandas_index(ds, include_start=True): + """Convert xarray time index to pandas datetime index. + + Parameters + ---------- + ds : xarray.Dataset + dataset with time index + include_start : bool, optional + include the start time in the index, by default True + + Returns + ------- + pd.DatetimeIndex + pandas datetime index + """ + if include_start: + return ds.time.to_index().insert(0, pd.Timestamp(ds.time.start)) + else: + return ds.time.to_index() From bbe77b8942a7eabe21984d07f1c6f0ef1db95d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Wed, 3 Jul 2024 16:56:21 +0200 Subject: [PATCH 72/85] fix UserWarning add_min_ahn_to_gdf() - fixes the warning - allows flox to be used for faster groupby operations --- nlmod/gwf/surface_water.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index b912490e..c4ab4cbb 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -922,7 +922,8 @@ def add_min_ahn_to_gdf(gdf, ahn, buffer=0.0, column="ahn_min"): rasterize_function=partial(rasterize_image, all_touched=True), ) gc["ahn"] = ahn - + # NOTE: removes UserWarning and ensures groupby with flox does not error: + gc = gc.isel(band=0).set_coords("index") ahn_min = gc.groupby("index").min()["ahn"].to_pandas() ahn_min.index = ahn_min.index.astype(int) gdf[column] = ahn_min From 87a3768f45f818e2525412e1dc940b47d509dc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Wed, 3 Jul 2024 16:58:51 +0200 Subject: [PATCH 73/85] docs and logging utils - Improve docststring get_exe_path - lower log-level get_flopy_bin_directories to DEBUG --- nlmod/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nlmod/util.py b/nlmod/util.py index ed052967..7189f351 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -106,10 +106,10 @@ def get_exe_path( Searching for the executables is done in the following order: 0. If exe_name is a full path, return the full path of the executable. 1. The directory specified with `bindir`. Raises error if exe_name is provided - and not found. Requires enable_version_check to be False. + and not found. 2. The directory used by nlmod installed in this environment. 3. If the executables were downloaded with flopy/nlmod from an other env, - most recent installation location of MODFLOW is found in flopy metadata + most recent installation location of MODFLOW is found in flopy metadata Else: 4. Download the executables using `version_tag` and `repo`. From 45c25be966c59fd124dfe7261e10db660a845023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Wed, 3 Jul 2024 16:59:01 +0200 Subject: [PATCH 74/85] docs and logging utils - Improve docststring get_exe_path - lower log-level get_flopy_bin_directories to DEBUG --- nlmod/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlmod/util.py b/nlmod/util.py index 7189f351..cfa273e7 100644 --- a/nlmod/util.py +++ b/nlmod/util.py @@ -377,7 +377,7 @@ def get_flopy_bin_directories(version_tag=None, repo="executables"): "`version_tag` is not passed to `get_flopy_bin_directories()`." ) meta_list_validversion = meta_list - logger.info(msg) + logger.debug(msg) path_list = [ Path(meta["bindir"]) From c66bd7c921e59cbd0891f628805d202586793466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Wed, 3 Jul 2024 17:00:12 +0200 Subject: [PATCH 75/85] Improve examples - typos - numpy 2.0 - show pkg versions at top --- docs/examples/00_model_from_scratch.ipynb | 3 +- docs/examples/01_basic_model.ipynb | 19 ++++----- docs/examples/02_surface_water.ipynb | 19 +++++---- docs/examples/03_local_grid_refinement.ipynb | 14 +++---- docs/examples/04_modifying_layermodels.ipynb | 40 ++++++++++++------ docs/examples/05_caching.ipynb | 34 +++++++++------- docs/examples/06_gridding_vector_data.ipynb | 30 +++++++++----- docs/examples/07_resampling.ipynb | 17 ++++---- docs/examples/08_gis.ipynb | 5 +-- docs/examples/09_schoonhoven.ipynb | 21 +++++----- docs/examples/10_modpath.ipynb | 13 +++--- docs/examples/11_grid_rotation.ipynb | 43 ++++++-------------- docs/examples/12_layer_generation.ipynb | 5 +-- docs/examples/13_plot_methods.ipynb | 7 ++-- docs/examples/14_stromingen_example.ipynb | 15 +++++-- docs/examples/15_geotop.ipynb | 15 +++---- docs/examples/16_groundwater_transport.ipynb | 15 +++++-- docs/examples/17_unsaturated_zone_flow.ipynb | 26 ++++-------- 18 files changed, 177 insertions(+), 164 deletions(-) diff --git a/docs/examples/00_model_from_scratch.ipynb b/docs/examples/00_model_from_scratch.ipynb index f452a9e5..81842350 100644 --- a/docs/examples/00_model_from_scratch.ipynb +++ b/docs/examples/00_model_from_scratch.ipynb @@ -31,7 +31,8 @@ "metadata": {}, "outputs": [], "source": [ - "nlmod.util.get_color_logger(\"INFO\");" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { diff --git a/docs/examples/01_basic_model.ipynb b/docs/examples/01_basic_model.ipynb index a8bd308e..a34a9695 100644 --- a/docs/examples/01_basic_model.ipynb +++ b/docs/examples/01_basic_model.ipynb @@ -27,9 +27,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\")" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -40,17 +39,17 @@ "\n", "With the code below we create a modflow model with the name 'IJmuiden'. This model has the following properties :\n", "- an extent that covers part of the Northsea, Noordzeekanaal and the small port city IJmuiden.\n", - "- a structured grid based on the subsurface models [Regis](https://www.dinoloket.nl/regis-ii-het-hydrogeologische-model) and [Geotop](https://www.dinoloket.nl/detaillering-van-de-bovenste-lagen-met-geotop). The Regis layers that are not present within the extent are removed. In this case we use 'MSz1' as the bottom layer of the model. Use `nlmod.read.regis.get_layer_names()` to get all the layer names of Regis. All Regis layers below this layer are not used in the model. Geotop is used to replace the holoceen layer in Regis because there is no kh or kv defined for the holoceen in Regis. Part of the model is in the North sea. Regis and Geotop have no data there. Therefore the Regis and Geotop layers are extrapolated from the shore and the seabed is added using bathymetry data from [Jarkus](https://www.openearth.nl/rws-bathymetry/2018.html).\n", + "- a structured grid based on the subsurface models [Regis](https://www.dinoloket.nl/regis-ii-het-hydrogeologische-model) and [Geotop](https://www.dinoloket.nl/detaillering-van-de-bovenste-lagen-met-geotop). The Regis layers that are not present within the extent are removed. In this case we use 'MSz1' as the bottom layer of the model. Use `nlmod.read.regis.get_layer_names()` to get all the layer names of Regis. All Regis layers below this layer are not used in the model. Geotop is used to replace the Holocene layer in Regis because there is no kh or kv defined for the Holocene in Regis. Part of the model is in the North sea. Regis and Geotop have no data there. Therefore the Regis and Geotop layers are extrapolated from the shore and the seabed is added using bathymetry data from [Jarkus](https://www.openearth.nl/rws-bathymetry/2018.html).\n", "- starting heads of 1 in every cell.\n", - "- the model is a steady state model of a single time step.\n", + "- the model is a steady state model with a single time step.\n", "- big surface water bodies (Northsea, IJsselmeer, Markermeer, Noordzeekanaal) within the extent are added as a general head boundary. The surface water bodies are obtained from a [shapefile](..\\data\\shapes\\opp_water.shp).\n", - "- surface drainage is added using [ahn](https://www.ahn.nl) data and a default conductance of $1000 m^2/d$\n", - "- recharge is added using data from the [knmi](https://www.knmi.nl/nederland-nu/klimatologie/daggegevens) using the following steps:~~\n", + "- surface drainage is added using the Dutch DEM ([ahn](https://www.ahn.nl)) and a default conductance of $1000 m^2/d$\n", + "- recharge is added using data from [knmi](https://www.knmi.nl/nederland-nu/klimatologie/daggegevens) using the following steps:~~\n", " 1. Check for each cell which KNMI weather and/or rainfall station is closest.\n", " 2. Download the data for the stations found in 1. for the model period. For a steady state stress period the average precipitation and evaporation of 8 years before the stress period time is used.\n", - " 3. Combine precipitation and evaporation data from step 2 to create a recharge time series for each cell\n", + " 3. Combine precipitation and evaporation data from step 2 to create a recharge time series for each cell,\n", " 4. Add the timeseries to the model dataset and create the recharge package.\n", - "- constant head boundaries are added to the model edges in every layer. The starting head is used as constant head." + "- constant head boundaries are added to the model edges in every layer. The starting head is used as the specified head." ] }, { @@ -244,7 +243,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Data from a model with a structured grid can be easily visualised using the model dataset. Below some examples" + "Data from a model with a structured grid can be easily visualised using the model dataset. Below are some examples:" ] }, { diff --git a/docs/examples/02_surface_water.ipynb b/docs/examples/02_surface_water.ipynb index ab34565f..d89ae27f 100644 --- a/docs/examples/02_surface_water.ipynb +++ b/docs/examples/02_surface_water.ipynb @@ -12,7 +12,7 @@ "\n", "This example notebook shows some how to add surface water defined in a GeoDataFrame to a MODFLOW model using the `nlmod` package.\n", "\n", - "There are three water boards in the model area, of which we download seasonal data about the stage of the surface water. In this notebook we perform a steady-state run, in which the stage of the surface water is the mean of the summer and winter stage. For locations without a stage from the water board, we delineate information from a Digital Terrain Model, to set a stage. We assign a stage of 0.0 m NAP to the river Lek. to The surface water bodies in each cell are aggregated using an area-weighted method and added to the model as a river-package." + "There are three water boards in the model area, and we download seasonal data about the stage of the surface water for each. In this notebook we perform a steady-state run, in which the stage of the surface water is the mean of the summer and winter stage. For locations without a stage from the water board, we obtain information from a Digital Terrain Model near the surface water features, to estimate a stage. We assign a stage of 0.0 m NAP to the river Lek. The surface water bodies in each cell are aggregated using an area-weighted method and added to the model with the river-package." ] }, { @@ -38,9 +38,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\")" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -101,7 +100,7 @@ "metadata": {}, "source": [ "### Layer 'waterdeel' from bgt\n", - "As the source of the location of the surface water bodies we use the 'waterdeel' layer of the Basisregistratie Grootschalige Topografie (BGT). This data consists of detailed polygons, maintained by dutch government agencies (water boards, municipalities and Rijkswatrstaat)." + "As the source of the location of the surface water bodies we use the 'waterdeel' layer of the Basisregistratie Grootschalige Topografie (BGT). This data consists of detailed polygons, maintained by dutch government agencies (water boards, municipalities and Rijkswaterstaat)." ] }, { @@ -221,7 +220,7 @@ "metadata": {}, "source": [ "#### Save the data to use in other notebooks as well\n", - "We save the bgt-data to a GeoPackage file, so we can use the data in other notebooks with surface water as well" + "We save the bgt-data to a GeoPackage file, so we can use the data in other notebooks with surface water as well." ] }, { @@ -273,7 +272,13 @@ "\n", "The `stage` and the `botm` columns are present in our dataset. The bottom resistance `c0` is rarely known, and is usually estimated when building the model. We will add our estimate later on.\n", "\n", - "*__Note__: the NaN's in the dataset indicate that not all parameters are known for each feature. This is not necessarily a problem but this will mean some features will not be converted to model input.*" + "
\n", + " \n", + "Note:\n", + "\n", + "The NaN's in the dataset indicate that not all parameters are known for each feature. This is not necessarily a problem but this will mean some features will not be converted to model input.\n", + " \n", + "
" ] }, { diff --git a/docs/examples/03_local_grid_refinement.ipynb b/docs/examples/03_local_grid_refinement.ipynb index 9fed4ef6..45842ce5 100644 --- a/docs/examples/03_local_grid_refinement.ipynb +++ b/docs/examples/03_local_grid_refinement.ipynb @@ -36,9 +36,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\")" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -47,7 +46,7 @@ "source": [ "## Create model\n", "\n", - "Modflow 6 makes it possible to use locally refined grids. In `nlmod` you can use a shapefile and a number of levels to specify where and how much you want to use local grid refinement. Below we use a shapefile of the Planetenweg in IJmuiden and set the refinement levels at 2. This well create a grid with cells of 100x100m except at the Planetenweg where the cells will be refined to 25x25m. See also figures below." + "Modflow 6 makes it possible to use locally refined grids. In `nlmod` you can use a shapefile and a number of levels to specify where and how much you want to use local grid refinement. Below we use a shapefile of the Planetenweg in IJmuiden and set the refinement levels at 2. This well create a grid with cells of 100x100m except at the Planetenweg where the cells will be refined to 25x25m. See figures below." ] }, { @@ -117,7 +116,7 @@ "source": [ "## Local grid refinement\n", "\n", - "the code below applies a local grid refinement to the layer model. The local grid refinement is based on the shapefile 'planetenweg_ijmuiden.shp', which contains a line shape of the Planetenweg, and the levels, which is 2. This means that the model cells at the Planetenweg will get a size of 25 x 25m. " + "The code below applies a local grid refinement to the layer model. The local grid refinement is based on the shapefile 'planetenweg_ijmuiden.shp', which contains a line shape of the Planetenweg, and the levels, which is 2. This means that the model cells at the Planetenweg will get a size of 25 x 25m because we halving the cell size twice (100 / (2^2) = 25). " ] }, { @@ -313,7 +312,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "There is also the option to create an animation of a cross section" + "There is also the option to create an animation of a cross section:" ] }, { @@ -330,7 +329,8 @@ "f, ax = plt.subplots(figsize=(10, 6))\n", "dcs = nlmod.plot.DatasetCrossSection(ds, line, ax=ax, zmin=-30.0, zmax=5.0)\n", "\n", - "# plot a map with the locaton of the cross-section (which is shown below the cross-section)\n", + "# plot a map with the locaton of the cross-section (which is shown below the\n", + "# cross-section)\n", "dcs.plot_map_cs(lw=5, figsize=10)\n", "\n", "# add labels with layer names\n", diff --git a/docs/examples/04_modifying_layermodels.ipynb b/docs/examples/04_modifying_layermodels.ipynb index f8ef9abd..9233c982 100644 --- a/docs/examples/04_modifying_layermodels.ipynb +++ b/docs/examples/04_modifying_layermodels.ipynb @@ -33,9 +33,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\");" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -188,9 +187,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "First we determine how to split the layers. This is done by creating a list of factors, that is used to determine fractions that add up to 1. The layer will be split into sub-layers from the top down, with each sub-layer getting a thickness equal to the fraction times the original thickness.\n", + "First we determine how to split the layers. This is done by creating a list of factors,\n", + "that is used to determine fractions that add up to 1. The layer will be split into\n", + "sub-layers from the top down, with each sub-layer getting a thickness equal to the\n", + "fraction times the original thickness.\n", "\n", - "For example, `(1, 1)` will split the layer into two sub-layers, each getting a thickness equal to 50% of the original layer." + "For example, `(1, 1)` will split the layer into two sub-layers, each getting a\n", + "thickness equal to 50% of the original layer. In this example the fractions already add\n", + "up to 1 for each layer." ] }, { @@ -200,7 +204,10 @@ "outputs": [], "source": [ "# split dictionary\n", - "split_dict = {\"PZWAz2\": (0.3, 0.3, 0.4), \"PZWAz3\": (0.2, 0.2, 0.2, 0.2, 0.2)}" + "split_dict = {\n", + " \"PZWAz2\": (0.3, 0.3, 0.4),\n", + " \"PZWAz3\": (0.2, 0.2, 0.2, 0.2, 0.2),\n", + "}" ] }, { @@ -241,7 +248,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The reindexer dictionary we stored links the new layers to the old layers. This can be convenient for copying data from the original layers to the new sub-layers." + "The reindexer dictionary links the new layers to the old layers. This can be convenient\n", + "for copying data from the original layers to the new sub-layers." ] }, { @@ -365,9 +373,15 @@ "source": [ "## Set new model top\n", "\n", - "The nlmod.layers.set_model_top changes the top of the model. When the new top is lower than the old top, the new top is burned in the layer model, lowering the top of the top layer(s). Top layers can become incactive, when the thickness is reduced to 0. When the new top is higher than the old top, the thickness of the most upper active layer (not necessarily the first) is increased. This method can be used to change the model top to a digital terrain model with a higher accuracy.\n", + "The `nlmod.layers.set_model_top` changes the top of the model. When the new top is\n", + "lower than the old top, the new top is burned in the layer model, lowering the top of\n", + "the top layer(s). Top layers can become incactive, when the thickness is reduced to 0.\n", + "When the new top is higher than the old top, the thickness of the most upper active\n", + "layer (not necessarily the first) is increased. This method can be used to change the\n", + "model top to a digital terrain model with a higher accuracy.\n", "\n", - "First transform the regis-date to a model Dataset, as the next methods need a model Dataset." + "First transform the regis-date to a model Dataset, as the next methods need a model\n", + "Dataset." ] }, { @@ -394,7 +408,7 @@ "metadata": {}, "source": [ "## Set layer top\n", - "nlmod.layers.set_layer_top sets the layer top to a specified value or array.\n", + "`nlmod.layers.set_layer_top` sets the layer top to a specified value or array.\n", "\n", "This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before." ] @@ -414,7 +428,7 @@ "metadata": {}, "source": [ "## Set layer bottom\n", - "nlmod.layers.set_layer_botm sets the layer botm to a specified value or array.\n", + "`nlmod.layers.set_layer_botm` sets the layer botm to a specified value or array.\n", "\n", "This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before." ] @@ -435,7 +449,7 @@ "metadata": {}, "source": [ "## Set layer thickness\n", - "nlmod.layers.set_layer_thickness sets the thickness of a layer to a specified value or array. With a parameter called 'change' you can specify in which direction the layer is changed. The only supported option for now is 'botm', which changes the layer botm. \n", + "`nlmod.layers.set_layer_thickness` sets the thickness of a layer to a specified value or array. With a parameter called 'change' you can specify in which direction the layer is changed. The only supported option for now is 'botm', which changes the layer botm. \n", "\n", "This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before." ] @@ -456,7 +470,7 @@ "metadata": {}, "source": [ "## Set minimum layer thickness\n", - "nlmod.layers.set_minimum layer_thickness increases the thickness of a layer if the thickness is less than a specified value. With a parameter called 'change' you can specify in which direction the layer is changed. The only supported option for now is 'botm', which changes the layer botm. \n", + "`nlmod.layers.set_minimum layer_thickness` increases the thickness of a layer if the thickness is less than a specified value. With a parameter called 'change' you can specify in which direction the layer is changed. The only supported option for now is 'botm', which changes the layer botm. \n", "\n", "This method only changes the shape of the layer, and does not check if all hydrological properties are defined for cells that had a thickness of 0 before." ] diff --git a/docs/examples/05_caching.ipynb b/docs/examples/05_caching.ipynb index f2e2de21..bca785e5 100644 --- a/docs/examples/05_caching.ipynb +++ b/docs/examples/05_caching.ipynb @@ -10,7 +10,11 @@ "\n", "*O.N. Ebbens, Artesia, 2021*\n", "\n", - "Groundwater flow models are often data-intensive. Execution times can be shortened significantly by caching data. This notebooks explains how this caching is implemented in `nlmod`. The first three chapters explain how to use the caching in nlmod. The last chapter contains more technical details on the implementation and limitations of caching in nlmod." + "Groundwater flow models are often data-intensive. Execution times can be shortened\n", + "significantly by caching data. This notebooks explains how this caching is implemented\n", + "in `nlmod`. The first three sections explain how to use the caching in nlmod. The last\n", + "section contains more technical details on the implementation and limitations of\n", + "caching in nlmod." ] }, { @@ -32,8 +36,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "nlmod.util.get_color_logger(\"INFO\")" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -167,14 +171,14 @@ "### Caching functions\n", "\n", "The following functions use the caching as described above:\n", - "- nlmod.read.regis.get_combined_layer_models\n", - "- nlmod.read.regis.get_regis\n", - "- nlmod.read.rws.get_surface_water\n", - "- nlmod.read.rws.get_northsea\n", - "- nlmod.read.knmi.get_recharge\n", - "- nlmod.read.jarkus.get_bathymetry\n", - "- nlmod.read.geotop.get_geotop\n", - "- nlmod.read.ahn.get_ahn" + "- `nlmod.read.regis.get_combined_layer_models`\n", + "- `nlmod.read.regis.get_regis`\n", + "- `nlmod.read.rws.get_surface_water`\n", + "- `nlmod.read.rws.get_northsea`\n", + "- `nlmod.read.knmi.get_recharge`\n", + "- `nlmod.read.jarkus.get_bathymetry`\n", + "- `nlmod.read.geotop.get_geotop`\n", + "- `nlmod.read.ahn.get_ahn`" ] }, { @@ -183,12 +187,12 @@ "source": [ "## Checking the cache\n", "One of the steps in the caching process is to check if the cache was created using the same function arguments as the current function call. This check has some limitations:\n", - "- Only function arguments with certain types are checked. These types include: int, float, bool, str, bytes, list, tuple, dict, numpy.ndarray, xarray.DataArray and xarray.Dataset. If a function argument has a different type the cache is never used. In time more types can be added to the checks.\n", + "- Only function arguments with certain types are checked. These types include: int, float, bool, str, bytes, list, tuple, dict, numpy.ndarray, xarray.DataArray and xarray.Dataset. If a function argument has a different type the cache is never used. In future development more types may be added to the checks.\n", "- If one of the function arguments is an xarray Dataset the check is somewhat different. For a dataset we only check if it has identical dimensions and coordinates as the cached netcdf file. There is no check if the variables in the dataset are identical.\n", "- It is not possible to cache the results of a function with more than one xarray Dataset as an argument. This is due to the difference in checking datasets. If more than one xarray dataset is given the cache decoraters raises a TypeError.\n", "- If one of the function arguments is a filepath of type str we only check if the cached filepath is the same as the current filepath. We do not check if any changes were made to the file after the cache was created.\n", "\n", - "You can test how the caching works in different situation by running the function below a few times with different function arguments. The logs provide some information about using the cache or not." + "You can test how the caching works in different situations by running the function below a few times with different function arguments. The logs provide some information about using the cache or not." ] }, { @@ -346,9 +350,9 @@ "1. All function arguments are pickled and saved together with the netcdf file. If the function arguments use a lot of memory this process can be become slow. This should be taken into account when you decide to use caching.\n", "2. Function arguments that cannot be pickled using the `pickle` module raise an error in the caching process.\n", "3. A function with mutable function arguments that are modified during function execution should not be used in caching. It can be used but the cache will never be used. The check on function arguments will always be False since the original function arguments are compared with the modified function argument.\n", - "4. If one of the function arguments is an xarray Dataset we only check if the dataset has the same dimensions and coordinates as the cached netcdf file. There is no check on the variables (DataArrays) in the dataset because it would simply take too much time to check all the variables in the dataset. Also, most of the time it is not necesary to check all the variables as they are not used to create the cached file. There is one example where a variable from the dataset is used to create the cached file. The `nlmod.read.jarkus.get_bathymetry` uses the 'Northsea' DataArray to create a bathymetry dataset. When we access the 'Northsea' DataArray using `ds['Northsea']` in the `get_bathymetry` function there would be no check if the 'Northsea' DataArray that was used to create the cache is the same as the 'Northsea' DataArray in the current function call. The current solution for this is to make the 'Northsea' DataArray a separate function argument in the `get_bathymetry` function. This makes it also more clear which data is used in the function.\n", + "4. If one of the function arguments is an xarray Dataset we only check if the dataset has the same dimensions and coordinates as the cached netcdf file. There is no check on the variables (DataArrays) in the dataset because it would simply take too much time to check all the variables in the dataset. Also, most of the time it is not necessary to check all the variables as they are not used to create the cached file. There is one example where a variable from the dataset is used to create the cached file. The `nlmod.read.jarkus.get_bathymetry` uses the 'Northsea' DataArray to create a bathymetry dataset. When we access the 'Northsea' DataArray using `ds['Northsea']` in the `get_bathymetry` function there would be no check if the 'Northsea' DataArray that was used to create the cache is the same as the 'Northsea' DataArray in the current function call. The current solution for this is to make the 'Northsea' DataArray a separate function argument in the `get_bathymetry` function. This makes it also more clear which data is used in the function.\n", "5. There is a check to see if the module where the function is defined has been changed since the cache was created. This helps not to use the cache when changes are made to the function. Unfortunately when the function uses other functions from different modules these other modules are not checked for recent changes.\n", - "6. The `cache_netcdf` decorator uses `functools.wraps` and some home made magic to add properties, such as the name and the docstring, of the original function to the decorated function. This assumes that the original function has a docstring with a \"Returns\" heading. If this is not the case the docstring is not modified." + "6. The `cache_netcdf` decorator uses `functools.wraps` and some homemade magic to add properties, such as the name and the docstring, of the original function to the decorated function. This assumes that the original function has a docstring with a \"Returns\" heading. If this is not the case the docstring is not modified." ] }, { diff --git a/docs/examples/06_gridding_vector_data.ipynb b/docs/examples/06_gridding_vector_data.ipynb index 56104eb7..781e84ee 100644 --- a/docs/examples/06_gridding_vector_data.ipynb +++ b/docs/examples/06_gridding_vector_data.ipynb @@ -29,6 +29,15 @@ "import nlmod" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "nlmod.show_versions()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -56,9 +65,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Vector to grid\n", - "\n", - "Vector data can be points, lines or polygons often saved as shapefiles and visualised using GIS software. A common operation is to project vector data on a modelgrid. For example to add a surface water line to a grid. Here are some functions in `nlmod` to project vector data on a modelgrid." + "## Vector to grid" ] }, { @@ -322,7 +329,7 @@ "source": [ "### Aggregate parameters per model cell\n", "\n", - "Aggregatie options:\n", + "Aggregation options:\n", "- point: max, min, mean\n", "- line: max, min, length_weighted, max_length\n", "- polygon: max, min, area_weighted, area_max\n" @@ -366,7 +373,7 @@ "metadata": {}, "source": [ "## Grid to reclist\n", - "For some modflow packages (drn, riv, ghb, wel) you need to specify stress_period_data to create them using flopy. This stress_period_data consists of reclists (also called lrcd for a structured grid) for every time step. \n", + "For some modflow packages (drn, riv, ghb, wel) you need to specify stress_period_data to create them using flopy. This stress_period_data consists of lists of records, known as reclists (also called lrcd (\"layer, row, column-data\") for a structured grid), for every time step.\n", "\n", "The function `da_to_reclist` can be used to convert grid data (both structured and vertex) to a reclist. This function has many arguments:\n", "- `mask`, boolean DataArray to determine which cells should be added to the reclist. Can be 2d or 3d.\n", @@ -389,12 +396,13 @@ " ds = ds.expand_dims({\"layer\": range(3)})\n", "\n", "# create some data arrays\n", + "rng = np.random.default_rng(12345)\n", "ds[\"da1\"] = (\n", " (\"layer\", \"y\", \"x\"),\n", - " np.random.randint(0, 10, (ds.sizes[\"layer\"], ds.sizes[\"y\"], ds.sizes[\"x\"])),\n", + " rng.integers(0, 10, (ds.sizes[\"layer\"], ds.sizes[\"y\"], ds.sizes[\"x\"])),\n", ")\n", - "ds[\"da2\"] = (\"y\", \"x\"), np.random.randint(0, 10, (ds.sizes[\"y\"], ds.sizes[\"x\"]))\n", - "ds[\"da3\"] = (\"y\", \"x\"), np.random.randint(0, 10, (ds.sizes[\"y\"], ds.sizes[\"x\"]))\n", + "ds[\"da2\"] = (\"y\", \"x\"), rng.integers(0, 10, (ds.sizes[\"y\"], ds.sizes[\"x\"]))\n", + "ds[\"da3\"] = (\"y\", \"x\"), rng.integers(0, 10, (ds.sizes[\"y\"], ds.sizes[\"x\"]))\n", "\n", "# add a nodata value\n", "ds.attrs[\"nodata\"] = -999\n", @@ -509,9 +517,9 @@ "metadata": {}, "source": [ "### Reclist columns\n", - "The `col1`, `col2` and `col3` arguments specify what data should be listed in the reclist. The types can be `str`,`xarray.DataArray`,`None` or other. If the value is a `str` the corresponding DataArray from the Dataset is used to get data for the reclist. If the value is an `xarray.DataArray` the DataArray is used. If the value is `None` the column is not added to the reclist and if the value is from another type the value is used for every record in the reclist.\n", + "The `col1`, `col2` and `col3` arguments specify what data should be put into the reclist. The types can be `str`,`xarray.DataArray`,`None` or other. If the value is a `str` the corresponding DataArray from the Dataset is used to get data for the reclist. If the value is an `xarray.DataArray` the DataArray is used. If the value is `None` the column is not added to the reclist and if the value is another type that value is used for every record in the reclist.\n", "\n", - "Be aware that if `mask` is a 3d array, the DataArrays of the column should also be 3d." + "Be aware that if `mask` is a 3d array, and the DataArrays of the column should also be 3d." ] }, { @@ -542,7 +550,7 @@ "outputs": [], "source": [ "# add some random DataArray to the vertex dataset\n", - "da_vert = np.random.randint(0, 10, (dsv[\"area\"].shape))\n", + "da_vert = rng.integers(0, 10, (dsv[\"area\"].shape))\n", "dsv[\"da_vert\"] = (\"icell2d\"), da_vert\n", "\n", "# create rec list from a vertex dataset\n", diff --git a/docs/examples/07_resampling.ipynb b/docs/examples/07_resampling.ipynb index bb9a5ce9..43d71380 100644 --- a/docs/examples/07_resampling.ipynb +++ b/docs/examples/07_resampling.ipynb @@ -18,7 +18,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "import geopandas as gpd\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", @@ -37,9 +36,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\");" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -48,11 +46,11 @@ "source": [ "## Grid types\n", "\n", - "So far two different gridtypes are supported in `nlmod`:\n", + "Two different gridtypes are supported in `nlmod`:\n", "- structured grids where the cellsize is fixed for all cells\n", "- vertex grids where the cellsize differs locally. These grids are usually created using local grid refinement algorithms.\n", "\n", - "In this notebook we define a few xarray dataarray of structured and vertex grids. We use these grids in the next chapter to show the resampling functions in `nlmod`." + "In this notebook we define a few xarray DataArrays of structured and vertex grids. We use these grids in the next section to show the resampling functions in `nlmod`." ] }, { @@ -71,7 +69,8 @@ "outputs": [], "source": [ "ds = nlmod.get_ds([950, 1250, 20050, 20350], delr=100)\n", - "ds[\"data\"] = (\"y\", \"x\"), np.random.rand(len(ds.y), len(ds.x)) * 10\n", + "rng = np.random.default_rng(12345)\n", + "ds[\"data\"] = (\"y\", \"x\"), rng.random(len(ds.y), len(ds.x)) * 10\n", "\n", "fig, ax = plt.subplots()\n", "ax.set_aspect(\"equal\")\n", @@ -115,7 +114,7 @@ "dsv = nlmod.grid.refine(\n", " ds, refinement_features=[([Point(1200, 20200)], \"point\", 1)], model_ws=\"model7\"\n", ")\n", - "dsv[\"data\"] = \"icell2d\", np.random.rand(len(dsv.data))\n", + "dsv[\"data\"] = \"icell2d\", rng.random(len(dsv.data))\n", "\n", "fig, ax = plt.subplots()\n", "ax.set_aspect(\"equal\")\n", @@ -468,7 +467,7 @@ "metadata": {}, "source": [ "### Transform ahn data to structured grid\n", - "We crate a dummy dataset with a structured grid, to which we will resample the AHN-data" + "We create a dummy dataset with a structured grid, to which we will resample the AHN-data." ] }, { diff --git a/docs/examples/08_gis.ipynb b/docs/examples/08_gis.ipynb index 40c56731..8e89929d 100644 --- a/docs/examples/08_gis.ipynb +++ b/docs/examples/08_gis.ipynb @@ -33,9 +33,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\");" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { diff --git a/docs/examples/09_schoonhoven.ipynb b/docs/examples/09_schoonhoven.ipynb index 1a381db5..b1d22f9d 100644 --- a/docs/examples/09_schoonhoven.ipynb +++ b/docs/examples/09_schoonhoven.ipynb @@ -38,9 +38,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\")" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -80,7 +79,7 @@ "metadata": {}, "source": [ "### layer 'waterdeel' from bgt\n", - "As the source of the location of the surface water bodies we a GeoDataFrame we created in the the surface notebook. We then saved this data as a geosjon. We now read this data again." + "The location of the surface water bodies is obtained from the GeoDataFrame that was created in the the surface water notebook. We saved this data as a geosjon file and load it here." ] }, { @@ -106,7 +105,7 @@ "metadata": {}, "source": [ "#### Plot summer stage of surface water bodies\n", - "We can plot the summer stage. There are some surface water bodies without a summer-stage, because the 'bronhouder' is not a water board. The main one is the river Lek, but there are also some surface water bodies without a summer stage more north." + "We can plot the summer stage. There are some surface water bodies without a summer stage, because the 'bronhouder' is not a water board. The main one is the river Lek, but there are also some surface water bodies without a summer stage in the north of the model area." ] }, { @@ -174,7 +173,7 @@ "id": "9f64d15e", "metadata": {}, "source": [ - "We then create a regular grid, add nessecary variables (eg idomain) and fill nan's. For example, REGIS does not contain infomration about the hydraulic conductivity of the first layer ('HLc'). These NaN's are replaced by a default hydraulic conductivity (kh) of 1 m/d. This probably is not a good representation of the conductivity, but at least the model will run." + "We then create a regular grid, add necessary variables and fill NaN's. For example, REGIS does not contain information about the hydraulic conductivity of the first layer ('HLc'). These NaN's are replaced by a default hydraulic conductivity (kh) of 1 m/d. This probably is not a good representation of the conductivity, but at least the model will run." ] }, { @@ -253,7 +252,7 @@ "metadata": {}, "source": [ "## Create a groundwater flow model\n", - "Using the data from the xarray Dataset ds we generate a groundwater flow model." + "Using the data from the xarray Dataset `ds` we generate a groundwater flow model." ] }, { @@ -297,7 +296,7 @@ "metadata": {}, "source": [ "## Process surface water\n", - "We cut the surface water bodies with the grid, set a default resistance of 1 day, and seperate the large river 'Lek' form the other surface water bodies." + "We intersect the surface water bodies with the grid, set a default bed resistance of 1 day, and seperate the large river 'Lek' form the other surface water bodies." ] }, { @@ -371,7 +370,7 @@ "metadata": {}, "source": [ "### Other surface water as drains\n", - "model the other surface water using the drain package, with a summer stage and a winter stage" + "Model the other surface water using the drain package, with a summer stage and a winter stage" ] }, { @@ -391,7 +390,7 @@ "source": [ "### Add lake\n", "\n", - "Model de \"grote gracht\" and \"Oude Haven\" as lakes. Let the grote gracht overflow in to de oude Haven." + "Model de \"grote gracht\" and \"Oude Haven\" as lakes. Let the grote gracht overflow into the oude Haven." ] }, { @@ -753,7 +752,7 @@ "outputs": [], "source": [ "def get_segments(x, y, segments=None):\n", - " # split each flopath in multiple line segments\n", + " # split each flow path into multiple line segments\n", " return [np.column_stack([x[i : i + 2], y[i : i + 2]]) for i in range(len(x) - 1)]\n", "\n", "\n", diff --git a/docs/examples/10_modpath.ipynb b/docs/examples/10_modpath.ipynb index 0a4e77c9..408c5b6b 100644 --- a/docs/examples/10_modpath.ipynb +++ b/docs/examples/10_modpath.ipynb @@ -40,9 +40,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\")" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -51,7 +50,7 @@ "source": [ "## Groundwater Flow Model\n", "\n", - "We use the groundwater flow model from the [03_local_grid_refinement notebook](03_local_grid_refinement.ipynb). Make sure to run this notebook before you run this notebook" + "We use the groundwater flow model from the [03_local_grid_refinement notebook](03_local_grid_refinement.ipynb). Make sure to run that notebook before you run this notebook." ] }, { @@ -73,7 +72,7 @@ "metadata": {}, "outputs": [], "source": [ - "# load lgr simulation and groundwateflow model\n", + "# load simulation and groundwaterflow model\n", "# set exe_name to point to mf6 version in nlmod bin directory\n", "exe_name = os.path.join(os.path.dirname(nlmod.__file__), \"bin\", \"mf6\")\n", "if sys.platform.startswith(\"win\"):\n", @@ -177,7 +176,7 @@ "source": [ "f, ax = plt.subplots(nrows=1, ncols=1, figsize=(12, 4))\n", "\n", - "for i, pid in enumerate(np.unique(pdata[\"particleid\"])):\n", + "for _, pid in enumerate(np.unique(pdata[\"particleid\"])):\n", " pf = pdata[pdata[\"particleid\"] == pid]\n", " x0, y0, z0 = pf[[\"x\", \"y\", \"z\"]][0]\n", " distance = np.sqrt((pf[\"x\"] - x0) ** 2 + (pf[\"y\"] - y0) ** 2 + (pf[\"z\"] - z0) ** 2)\n", @@ -285,7 +284,7 @@ "source": [ "f, ax = plt.subplots(nrows=1, ncols=1, figsize=(12, 4))\n", "\n", - "for i, pid in enumerate(np.unique(pdata[\"particleid\"])):\n", + "for _, pid in enumerate(np.unique(pdata[\"particleid\"])):\n", " pf = pdata[pdata[\"particleid\"] == pid]\n", " x0, y0, z0 = pf[[\"x\", \"y\", \"z\"]][0]\n", " distance = np.sqrt((pf[\"x\"] - x0) ** 2 + (pf[\"y\"] - y0) ** 2 + (pf[\"z\"] - z0) ** 2)\n", diff --git a/docs/examples/11_grid_rotation.ipynb b/docs/examples/11_grid_rotation.ipynb index 8b2dc97f..c92da164 100644 --- a/docs/examples/11_grid_rotation.ipynb +++ b/docs/examples/11_grid_rotation.ipynb @@ -9,15 +9,15 @@ "\n", "Rotated grids are supported in nlmod. It is implemented in the following manner:\n", "\n", - "- angrot, xorigin and yorigin (naming equal to modflow 6) are added to the attributes of the model Dataset.\n", - "- angrot is the counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system (equal to definition in modflow 6)\n", + "- `angrot`, `xorigin` and `yorigin` (naming identical to modflow 6) are added to the attributes of the model Dataset.\n", + "- `angrot` is the counter-clockwise rotation angle (in degrees) of the model grid coordinate system relative to a real-world coordinate system (identical to definition in modflow 6)\n", "- when a grid is rotated:\n", - " - x and y (and xv and yv for a vertex grid) are in model-coordinates, instead of real-world-coordinates.\n", - " - xc and yc are added to the Dataset and represent the cell centers in real-world coordinates (naming equal to rioxarray rotated grids)\n", - " - the plot-methods in nlmod plot the grid in model-coordinates by default (can be overridden by the setting the parameter 'rotated' to True)\n", - " - before intersecting with the grid, GeoDataFrames are automtically transformed to model coordinates.\n", + " - `x` and `y` (and `xv` and `yv` for a vertex grid) are in model coordinates, instead of real-world coordinates.\n", + " - `xc` and `yc` are added to the Dataset and represent the cell centers in real-world coordinates (naming identical to rioxarray rotated grids)\n", + " - the plot-methods in nlmod plot the grid in model coordinates by default (can be overridden by the setting the parameter `rotated=True`)\n", + " - before intersecting with the grid, GeoDataFrames are automatically transformed to model coordinates.\n", "\n", - "When grids are not rotated, the model Dataset does not contain an attribute named 'angrot' (or its is 0). The x- and y-coordinates of the model then respresent real-world coordinates.\n", + "When grids are not rotated, the model Dataset does not contain an attribute named `angrot` (or it is 0). The x- and y-coordinates of the model then respresent real-world coordinates.\n", "\n", "In this notebook we generate a model of 1 by 1 km, with a grid that is rotated 10 degrees relative to the real-world coordinates system (EPSG:28992: RD-coordinates)." ] @@ -43,9 +43,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\");" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -74,25 +73,7 @@ " model_ws=\"model11\",\n", ")\n", "\n", - "# We add a time dimension to ds by adding one timestamp, with a value of\n", - "# 2023-1-1. Because the first stress period is steady state (default value of\n", - "# stady_start=True) and the default stress-period length of this period is 10\n", - "# years (steady_start_perlen=3652.0) the one and only stress period of the\n", - "# simulation will start at 2013-1-1 and end at 2023-1-1. Later in this notebook,\n", - "# this period will determine the recharge-value and the ratio between summer and\n", - "# winter stage of the surface water.\n", - "\n", - "ds = nlmod.time.set_ds_time(ds, time=\"2023-1-1\", start=\"2013\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d4ec49cf", - "metadata": {}, - "outputs": [], - "source": [ - "ds.time.start" + "ds = nlmod.time.set_ds_time(ds, time=\"2023-01-01\", start=\"2013-01-01\")" ] }, { @@ -101,7 +82,7 @@ "metadata": {}, "source": [ "## Use a disv-grid\n", - "We call the refine method to generate a vertex grid (with the option of grid-refinement), instead of a structured grid. We can comment the next line out, to keep a structured grid, and the rest of the notebook will run without problems as well." + "We call the refine method to generate a vertex grid (with the option of grid-refinement), instead of a structured grid. We can comment the next line to model a structured grid, and the rest of the notebook will run without problems as well." ] }, { @@ -303,7 +284,7 @@ "id": "0bf81227", "metadata": {}, "source": [ - "If we want to plot in realworld coordinates, we set the optional parameter 'rotated' to True." + "If we want to plot in realworld coordinates, we set the optional parameter `rotated=True`." ] }, { diff --git a/docs/examples/12_layer_generation.ipynb b/docs/examples/12_layer_generation.ipynb index f61f1e02..0daf9a51 100644 --- a/docs/examples/12_layer_generation.ipynb +++ b/docs/examples/12_layer_generation.ipynb @@ -38,9 +38,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\");" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { diff --git a/docs/examples/13_plot_methods.ipynb b/docs/examples/13_plot_methods.ipynb index 7c027527..20fa8bba 100644 --- a/docs/examples/13_plot_methods.ipynb +++ b/docs/examples/13_plot_methods.ipynb @@ -52,9 +52,8 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"nlmod version: {nlmod.__version__}\")\n", - "\n", - "nlmod.util.get_color_logger(\"INFO\");" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -62,7 +61,7 @@ "id": "f5ca2bf5", "metadata": {}, "source": [ - "First we read a fully run model, from the notebook 09_schoonhoven.ipynb. Please run this notebook first." + "First we read a fully run model, from the notebook 09_schoonhoven.ipynb. Please run that notebook first." ] }, { diff --git a/docs/examples/14_stromingen_example.ipynb b/docs/examples/14_stromingen_example.ipynb index 562b4575..63e60cab 100644 --- a/docs/examples/14_stromingen_example.ipynb +++ b/docs/examples/14_stromingen_example.ipynb @@ -9,7 +9,7 @@ "---\n", "\n", "This example is based on the essay _\"Open source grondwatermodellering met\n", - "MODFLOW 6\"_ that was recently published in Stromingen (Calje et al., 2022).\n", + "MODFLOW 6\"_ that was published in Stromingen (Calje et al., 2022).\n", "\n", "This example strives to achieve the simplicity of the example psuedo script\n", "that was shown in Figure 5 in the article. Some things require a bit more code\n", @@ -38,10 +38,17 @@ "import geopandas as gpd\n", "from pandas import date_range\n", "\n", - "import nlmod\n", - "\n", + "import nlmod" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "nlmod.util.get_color_logger(\"INFO\")\n", - "print(f\"nlmod version: {nlmod.__version__}\")" + "nlmod.show_versions()" ] }, { diff --git a/docs/examples/15_geotop.ipynb b/docs/examples/15_geotop.ipynb index 28584803..fb05dabc 100644 --- a/docs/examples/15_geotop.ipynb +++ b/docs/examples/15_geotop.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "source": [ "# Using information from GeoTOP\n", - "Most geohydrological models in the Netherlands use the layer model REGIS as the base for the geohydrological properties of the model. REGIS does not contain information for all layers though, of which the holocene confining layer (HLc) is the most important one. In this notebook we will show how to use the voxel model GeoTOP to generate geohydrolocal properties for this layer." + "Most geohydrological models in the Netherlands use the layer model REGIS as the basis for the geohydrological properties of the model. However, REGIS does not contain information for all layers, of which the holocene confining layer (HLc) is the most important one. In this notebook we will show how to use the voxel model GeoTOP to generate geohydrolocal properties for this layer." ] }, { @@ -30,7 +30,8 @@ "metadata": {}, "outputs": [], "source": [ - "nlmod.util.get_color_logger(\"INFO\");" + "nlmod.util.get_color_logger(\"INFO\")\n", + "nlmod.show_versions()" ] }, { @@ -97,7 +98,7 @@ "metadata": {}, "source": [ "## Download GeoTOP\n", - "We download GeoTOP for a certain extent. We get an xarray.Dataset with voxels of 100 * 100 * 0.5 (dx * dy * dz) m, with variables 'strat' and 'lithok'. We also get the probaliblity of the occurence of each lithoclass in variables named 'kans_*' (as `drop_probabilities` is `False`)." + "We download GeoTOP for a certain extent. We get an xarray.Dataset with voxels of 100 * 100 * 0.5 (dx * dy * dz) m, with variables 'strat' and 'lithok'. We also get the probaliblity of the occurence of each lithoclass in variables named 'kans_*' (since we set `probabilities=True`)." ] }, { @@ -146,7 +147,7 @@ "metadata": {}, "source": [ "### Based on lithok\n", - "With `nlmod.read.geotop.get_lithok_props()` we get a default value for each of the 9 lithoclasses (lthok 4 is not used). These values are a rough estimate of the hydrologic conductivity. We recommend changing these values based on local conditions." + "With `nlmod.read.geotop.get_lithok_props()` we get a default value for each of the 9 lithoclasses (lithoclass 4 is not used). These values are a rough estimate of the hydrologic conductivity. We recommend changing these values based on local conditions." ] }, { @@ -165,7 +166,7 @@ "id": "932b8994", "metadata": {}, "source": [ - "The method `nlmod.read.geotop.add_kh_and_kv` takes this DataFrame, applies it to the GeoTOP voxel-dataset `gt`, and adds the varaiables `kh` and `kv` to `gt`." + "The method `nlmod.read.geotop.add_kh_and_kv` takes this DataFrame, applies it to the GeoTOP voxel-dataset `gt`, and adds the variables `kh` and `kv` to `gt`." ] }, { @@ -212,7 +213,7 @@ "metadata": {}, "outputs": [], "source": [ - "df = nlmod.read.geotop.get_kh_kv_table()\n", + "df = nlmod.read.geotop.get_kh_kv_table(kind=\"Brabant\")\n", "df" ] }, @@ -402,7 +403,7 @@ "metadata": {}, "source": [ "## Using stochastic data from GeoTOP\n", - "In the previous section we used the most likely values from the lithoclass-data of GeoTOP. GeoTOP is constructed by generating 100 realisations of this data. Using these realisations a probablity is determined for the occurence in each pixel for each of the lithoclassses. We can also use these probabilities to determine the kh and kv-value of each voxel. We do this by settting the `stochastic` parameter in `nlmod.read.geotop.add_kh_and_kv` to True. The kh and kv values are now calculated by a weighted average of the lithoclass-data in each voxel, where the weights are determined by the probablilities. By default a arithmetic weighted mean is used for kh and a harmonic weighted mean for kv, but these methods can be chosen by the user." + "In the previous section we used the most likely values from the lithoclass data of GeoTOP. GeoTOP is constructed by generating 100 realisations of this data. Using these realisations a probablity is determined for the occurence in each pixel for each of the lithoclassses. We can also use these probabilities to determine the kh and kv-value of each voxel. We do this by settting the `stochastic` parameter in `nlmod.read.geotop.add_kh_and_kv` to True. The kh and kv values are now calculated by a weighted average of the lithoclass data in each voxel, where the weights are determined by the probablilities. By default an arithmetic weighted mean is used for kh and a harmonic weighted mean for kv, but these methods can be chosen by the user." ] }, { diff --git a/docs/examples/16_groundwater_transport.ipynb b/docs/examples/16_groundwater_transport.ipynb index d72ea7f8..912162d8 100644 --- a/docs/examples/16_groundwater_transport.ipynb +++ b/docs/examples/16_groundwater_transport.ipynb @@ -26,8 +26,15 @@ "import pandas as pd\n", "import xarray as xr\n", "\n", - "import nlmod\n", - "\n", + "import nlmod" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "# set up pretty logging and show package versions\n", "nlmod.util.get_color_logger(\"INFO\")\n", "nlmod.show_versions()" @@ -192,7 +199,7 @@ "metadata": {}, "outputs": [], "source": [ - "# we then determine the part of each cell that is covered by sea from the original fine ahn\n", + "# we then determine the part of each cell that is covered by sea from the original ahn\n", "ds[\"sea\"] = nlmod.read.rws.calculate_sea_coverage(ahn, ds=ds, method=\"average\")" ] }, @@ -411,7 +418,7 @@ "- The advection (ADV), dispersion (DSP), mass-storage transfer (MST) and\n", "source-sink mixing (SSM) packages each obtain information from the model\n", "dataset. These variables were defined by\n", - "nlmod.gwt.prepare.set_default_transport_parameters`. They can be also be\n", + "`nlmod.gwt.prepare.set_default_transport_parameters`. They can be also be\n", "modified or added to the dataset by the user. Another option is to directly\n", "pass the variables to the package constructors, in which case the stored values\n", "are ignored." diff --git a/docs/examples/17_unsaturated_zone_flow.ipynb b/docs/examples/17_unsaturated_zone_flow.ipynb index 76b21f03..00a01854 100644 --- a/docs/examples/17_unsaturated_zone_flow.ipynb +++ b/docs/examples/17_unsaturated_zone_flow.ipynb @@ -8,7 +8,7 @@ "# Unsaturated zone flow\n", "This notebook demonstrates the use of the Unsaturated Zone Flow (UZF) package in nlmod. The UZF-package can be used to simulate important processes in the unsaturated zone. These processes create a delay before precipitation reaches the saturated groundwater. In dry periods the water may even have evaporated before that. This notebook shows the difference in calculated head between a model with regular recharge (RCH) and evapotranspiration (EVT) packages, compared to a model with the UZF-package.\n", "\n", - "We create a 1d model, of 1 row and 1 column, but with multiple layers, of a real location somewhere in the Netherlands. We use weather data from the KNMI as input for a transient simulation of 3 years with daily timetseps. This notebook can be used to vary the uzf-parameter, change the location (do not forget to alter the drain-elevation as well), or to play with the model-timestep." + "We create a 1d model, consisting of 1 row and 1 column with multiple layers, of a real location somewhere in the Netherlands. We use weather data from the KNMI as input for a transient simulation of 3 years with daily timetseps. This notebook can be used to vary the uzf-parameters, change the location (do not forget to alter the drain-elevation as well), or to play with the model timestep." ] }, { @@ -33,27 +33,19 @@ "import numpy as np\n", "import pandas as pd\n", "\n", - "import nlmod\n", - "\n", - "# set up pretty logging and show package versions\n", - "nlmod.util.get_color_logger()\n", - "nlmod.show_versions()" + "import nlmod" ] }, { "cell_type": "code", "execution_count": null, - "id": "34f28281-fa68-4618-b063-8b9604306974", + "id": "febb2e33", "metadata": {}, "outputs": [], "source": [ - "# Ignore warning about 1d layers, in numpy 1.26.1 and flopy 3.4.3\n", - "import warnings\n", - "\n", - "warnings.filterwarnings(\n", - " \"ignore\",\n", - " message=\"Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future.\",\n", - ")" + "# set up pretty logging and show package versions\n", + "nlmod.util.get_color_logger()\n", + "nlmod.show_versions()" ] }, { @@ -178,7 +170,7 @@ "metadata": {}, "source": [ "## Generate a simulation (sim) and groundwater flow (gwf) object\n", - "We generate a model using with all basic packages. We add drainage level at 4.0 m NAP. As the top of our model is at 6.5 m NAP this will create an unsatuated zone of about 2.5 m." + "We generate a model using with all basic packages. We add a drainage level at 4.0 m NAP. As the top of our model is at 6.5 m NAP this will create an unsaturated zone of about 2.5 m." ] }, { @@ -281,7 +273,7 @@ "\n", "There can be multiple layers in the unsaturated zone, just like in the saturated zone. The method `nlmod.gwf.uzf` connects the unsaturated zone cells above each other.\n", "\n", - "Because we want to plot the water content in the subsurface we will add some observations of the water concent to the uzf-package. We do this by adding the optional parameter `obs_z` to `nlmod.gwf.uzf`. This will create the observations in the corresponding uzf-cells, at the requested z-values (in m NAP)." + "Because we want to plot the water content in the subsurface we will add some observations of the water content to the uzf-package. We do this by adding the optional parameter `obs_z` to `nlmod.gwf.uzf`. This will create the observations in the corresponding uzf-cells, at the requested z-values (in m NAP)." ] }, { @@ -371,7 +363,7 @@ "metadata": {}, "source": [ "## Compare models\n", - "We then make a plot to compare the heads in the two simulations we performed, and plot the water content we calculated in the UZF-calculation, and added observations for. We plot the water content in one vertical cell (the only) of the model. Figure layout thanks to Martin Vonk!\n", + "We then make a plot to compare the heads in the two simulations we performed, and plot the water content we calculated in the UZF-calculation, and added observations for. We plot the water content in one vertical cell of the model. Figure layout thanks to Martin Vonk!\n", "\n", "The figure shows that the effect of precipitation on the groundwater level is less in summer if we also take the effect of the unsaturated zone into account (using UZF). In dry periods precipitation never reaches the groundwater level, as evaporation takes place before that." ] From 46eaa261f206f2a283a7f9bb9df926e4fff9f734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Wed, 3 Jul 2024 17:02:58 +0200 Subject: [PATCH 76/85] version v0.8 --- nlmod/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nlmod/version.py b/nlmod/version.py index e281423c..213fe8f0 100644 --- a/nlmod/version.py +++ b/nlmod/version.py @@ -1,7 +1,7 @@ from importlib import metadata from platform import python_version -__version__ = "0.7.3b" +__version__ = "0.8.0" def show_versions() -> None: From 202b824bfbd4373f454b9061aef7bd8e856e70e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Calj=C3=A9?= Date: Thu, 4 Jul 2024 11:12:41 +0200 Subject: [PATCH 77/85] add_min_ahn_to_gdf would error with use of ahn-download (without saving to file) Which would result the following error ValueError: Dimensions {'band'} do not exist. Expected one or more of FrozenMappingWarningOnValuesAccess({'y': 600, 'x': 700}) --- docs/examples/02_surface_water.ipynb | 2 +- nlmod/gwf/surface_water.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/examples/02_surface_water.ipynb b/docs/examples/02_surface_water.ipynb index d89ae27f..15ed00ff 100644 --- a/docs/examples/02_surface_water.ipynb +++ b/docs/examples/02_surface_water.ipynb @@ -91,7 +91,7 @@ "if not os.path.isfile(fname_ahn):\n", " ahn = nlmod.read.ahn.get_ahn4(extent, identifier=\"AHN4_DTM_5m\")\n", " ahn.rio.to_raster(fname_ahn)\n", - "ahn = rioxarray.open_rasterio(fname_ahn, mask_and_scale=True)" + "ahn = rioxarray.open_rasterio(fname_ahn, mask_and_scale=True)[0]" ] }, { diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index c4ab4cbb..c88ff81b 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -922,8 +922,6 @@ def add_min_ahn_to_gdf(gdf, ahn, buffer=0.0, column="ahn_min"): rasterize_function=partial(rasterize_image, all_touched=True), ) gc["ahn"] = ahn - # NOTE: removes UserWarning and ensures groupby with flox does not error: - gc = gc.isel(band=0).set_coords("index") ahn_min = gc.groupby("index").min()["ahn"].to_pandas() ahn_min.index = ahn_min.index.astype(int) gdf[column] = ahn_min From 73b18d83046c08b68fd24554a51624abd7884522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Thu, 4 Jul 2024 11:52:24 +0200 Subject: [PATCH 78/85] fix examples: - rendering of numbered/bulleted lists - fix pyogrio warnings - other minor things --- docs/examples/01_basic_model.ipynb | 14 ++++++++------ docs/examples/05_caching.ipynb | 4 ++++ docs/examples/06_gridding_vector_data.ipynb | 2 ++ docs/examples/07_resampling.ipynb | 3 ++- docs/examples/08_gis.ipynb | 2 ++ docs/examples/09_schoonhoven.ipynb | 2 +- docs/examples/15_geotop.ipynb | 2 +- 7 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/examples/01_basic_model.ipynb b/docs/examples/01_basic_model.ipynb index a34a9695..63e5e5eb 100644 --- a/docs/examples/01_basic_model.ipynb +++ b/docs/examples/01_basic_model.ipynb @@ -37,18 +37,19 @@ "source": [ "## Create model\n", "\n", - "With the code below we create a modflow model with the name 'IJmuiden'. This model has the following properties :\n", + "With the code below we create a modflow model with the name 'IJmuiden'. This model has the following properties:\n", + "\n", "- an extent that covers part of the Northsea, Noordzeekanaal and the small port city IJmuiden.\n", "- a structured grid based on the subsurface models [Regis](https://www.dinoloket.nl/regis-ii-het-hydrogeologische-model) and [Geotop](https://www.dinoloket.nl/detaillering-van-de-bovenste-lagen-met-geotop). The Regis layers that are not present within the extent are removed. In this case we use 'MSz1' as the bottom layer of the model. Use `nlmod.read.regis.get_layer_names()` to get all the layer names of Regis. All Regis layers below this layer are not used in the model. Geotop is used to replace the Holocene layer in Regis because there is no kh or kv defined for the Holocene in Regis. Part of the model is in the North sea. Regis and Geotop have no data there. Therefore the Regis and Geotop layers are extrapolated from the shore and the seabed is added using bathymetry data from [Jarkus](https://www.openearth.nl/rws-bathymetry/2018.html).\n", "- starting heads of 1 in every cell.\n", "- the model is a steady state model with a single time step.\n", "- big surface water bodies (Northsea, IJsselmeer, Markermeer, Noordzeekanaal) within the extent are added as a general head boundary. The surface water bodies are obtained from a [shapefile](..\\data\\shapes\\opp_water.shp).\n", "- surface drainage is added using the Dutch DEM ([ahn](https://www.ahn.nl)) and a default conductance of $1000 m^2/d$\n", - "- recharge is added using data from [knmi](https://www.knmi.nl/nederland-nu/klimatologie/daggegevens) using the following steps:~~\n", - " 1. Check for each cell which KNMI weather and/or rainfall station is closest.\n", - " 2. Download the data for the stations found in 1. for the model period. For a steady state stress period the average precipitation and evaporation of 8 years before the stress period time is used.\n", - " 3. Combine precipitation and evaporation data from step 2 to create a recharge time series for each cell,\n", - " 4. Add the timeseries to the model dataset and create the recharge package.\n", + "- recharge is added using data from [knmi](https://www.knmi.nl/nederland-nu/klimatologie/daggegevens) using the following steps:\n", + " 1. Check for each cell which KNMI weather and/or rainfall station is closest.\n", + " 2. Download the data for the stations found in 1. for the model period. For a steady state stress period the average precipitation and evaporation of 8 years before the stress period time is used.\n", + " 3. Combine precipitation and evaporation data from step 2 to create a recharge time series for each cell,\n", + " 4. Add the timeseries to the model dataset and create the recharge package.\n", "- constant head boundaries are added to the model edges in every layer. The starting head is used as the specified head." ] }, @@ -208,6 +209,7 @@ "source": [ "## Write and Run\n", "Now that we've created all the modflow packages we need to write them to modflow files. You always have to write the modflow data to the model workspace before you can run the model. You can write the model files and run the model using the function `nlmod.sim.write_and_run)` as shown below. This function has two additional options:\n", + "\n", "1. Write the model dataset to the disk if `write_ds` is `True`. This makes it easier and faster to load model data if you ever need it. \n", "2. Write a copy of this Jupyter Notebook to the same directory as the modflow files if `nb_path` is the name of this Jupyter Notebook. It can be useful to have a copy of the script that created the modflow files, together with the files. " ] diff --git a/docs/examples/05_caching.ipynb b/docs/examples/05_caching.ipynb index bca785e5..e9888c41 100644 --- a/docs/examples/05_caching.ipynb +++ b/docs/examples/05_caching.ipynb @@ -146,6 +146,7 @@ "## Caching steps\n", "\n", "The netCDF caching is applied to a number of functions in nlmod that have an xarray dataset as output. When you call these functions using the `cachedir` and `cachename` arguments the following steps are taken:\n", + "\n", "1. See if there is a netCDF file with the specified cachename in the specified cache directory. If the file exists go to step 2, otherwise go to step 3.\n", "2. Read the netCDF file and return as an xarray dataset if:\n", " 1. The cached dataset was created using the same function arguments as the current function call. \n", @@ -171,6 +172,7 @@ "### Caching functions\n", "\n", "The following functions use the caching as described above:\n", + "\n", "- `nlmod.read.regis.get_combined_layer_models`\n", "- `nlmod.read.regis.get_regis`\n", "- `nlmod.read.rws.get_surface_water`\n", @@ -187,6 +189,7 @@ "source": [ "## Checking the cache\n", "One of the steps in the caching process is to check if the cache was created using the same function arguments as the current function call. This check has some limitations:\n", + "\n", "- Only function arguments with certain types are checked. These types include: int, float, bool, str, bytes, list, tuple, dict, numpy.ndarray, xarray.DataArray and xarray.Dataset. If a function argument has a different type the cache is never used. In future development more types may be added to the checks.\n", "- If one of the function arguments is an xarray Dataset the check is somewhat different. For a dataset we only check if it has identical dimensions and coordinates as the cached netcdf file. There is no check if the variables in the dataset are identical.\n", "- It is not possible to cache the results of a function with more than one xarray Dataset as an argument. This is due to the difference in checking datasets. If more than one xarray dataset is given the cache decoraters raises a TypeError.\n", @@ -347,6 +350,7 @@ "metadata": {}, "source": [ "### Properties\n", + "\n", "1. All function arguments are pickled and saved together with the netcdf file. If the function arguments use a lot of memory this process can be become slow. This should be taken into account when you decide to use caching.\n", "2. Function arguments that cannot be pickled using the `pickle` module raise an error in the caching process.\n", "3. A function with mutable function arguments that are modified during function execution should not be used in caching. It can be used but the cache will never be used. The check on function arguments will always be False since the original function arguments are compared with the modified function argument.\n", diff --git a/docs/examples/06_gridding_vector_data.ipynb b/docs/examples/06_gridding_vector_data.ipynb index 781e84ee..9a28073c 100644 --- a/docs/examples/06_gridding_vector_data.ipynb +++ b/docs/examples/06_gridding_vector_data.ipynb @@ -330,6 +330,7 @@ "### Aggregate parameters per model cell\n", "\n", "Aggregation options:\n", + "\n", "- point: max, min, mean\n", "- line: max, min, length_weighted, max_length\n", "- polygon: max, min, area_weighted, area_max\n" @@ -376,6 +377,7 @@ "For some modflow packages (drn, riv, ghb, wel) you need to specify stress_period_data to create them using flopy. This stress_period_data consists of lists of records, known as reclists (also called lrcd (\"layer, row, column-data\") for a structured grid), for every time step.\n", "\n", "The function `da_to_reclist` can be used to convert grid data (both structured and vertex) to a reclist. This function has many arguments:\n", + "\n", "- `mask`, boolean DataArray to determine which cells should be added to the reclist. Can be 2d or 3d.\n", "- `layer`, if `mask` is a 2d array the value of `layer` is used in the reclist. If `mask` is 3d or `first_active_layer` is True the `layer` argument is ignored.\n", "- `only_active_cells`, if True only add cells with an idomain of 1 to the reclist\n", diff --git a/docs/examples/07_resampling.ipynb b/docs/examples/07_resampling.ipynb index 43d71380..46dc9b84 100644 --- a/docs/examples/07_resampling.ipynb +++ b/docs/examples/07_resampling.ipynb @@ -47,6 +47,7 @@ "## Grid types\n", "\n", "Two different gridtypes are supported in `nlmod`:\n", + "\n", "- structured grids where the cellsize is fixed for all cells\n", "- vertex grids where the cellsize differs locally. These grids are usually created using local grid refinement algorithms.\n", "\n", @@ -70,7 +71,7 @@ "source": [ "ds = nlmod.get_ds([950, 1250, 20050, 20350], delr=100)\n", "rng = np.random.default_rng(12345)\n", - "ds[\"data\"] = (\"y\", \"x\"), rng.random(len(ds.y), len(ds.x)) * 10\n", + "ds[\"data\"] = (\"y\", \"x\"), rng.random((len(ds.y), len(ds.x))) * 10\n", "\n", "fig, ax = plt.subplots()\n", "ax.set_aspect(\"equal\")\n", diff --git a/docs/examples/08_gis.ipynb b/docs/examples/08_gis.ipynb index 8e89929d..78c615d1 100644 --- a/docs/examples/08_gis.ipynb +++ b/docs/examples/08_gis.ipynb @@ -270,6 +270,7 @@ "## Add symbology (QGIS)\n", "\n", "It is always nice to have automatic symbology for your vector data. Some thoughts:\n", + "\n", "- QGIS can save symbology of a single shapefile in a .qml file\n", "- In QGIS you can add a .qml file to a geopackage thus saving the symbology to a single file.\n", "- You can create a .qml file in QGIS from existing symbology.\n", @@ -281,6 +282,7 @@ "metadata": {}, "source": [ "Some limitations of the current gis functions:\n", + "\n", "- when exporting shapefiles to gis, attributes cannot have names longer\n", "than 10 characters. Now the automatic character shortening of fiona is\n", "used. This is not optimal.\n", diff --git a/docs/examples/09_schoonhoven.ipynb b/docs/examples/09_schoonhoven.ipynb index b1d22f9d..efdd02a3 100644 --- a/docs/examples/09_schoonhoven.ipynb +++ b/docs/examples/09_schoonhoven.ipynb @@ -424,7 +424,7 @@ ")\n", "\n", "# add lake to groundwaterflow model\n", - "nlmod.gwf.lake_from_gdf(gwf, lakes, ds, boundname_column=\"name\")" + "lak = nlmod.gwf.lake_from_gdf(gwf, lakes, ds, boundname_column=\"name\")" ] }, { diff --git a/docs/examples/15_geotop.ipynb b/docs/examples/15_geotop.ipynb index fb05dabc..d7bebc47 100644 --- a/docs/examples/15_geotop.ipynb +++ b/docs/examples/15_geotop.ipynb @@ -388,7 +388,7 @@ "var = \"kh\"\n", "norm = matplotlib.colors.Normalize(0.0, 40.0)\n", "\n", - "f, axes = nlmod.plot.get_map(extent, nrows=2, figsize=20)\n", + "f, axes = nlmod.plot.get_map(extent, nrows=2, figsize=(16, 8))\n", "pc = nlmod.plot.data_array(regis[var].loc[layer], ax=axes[0], norm=norm)\n", "nlmod.plot.colorbar_inside(pc, bounds=[0.02, 0.05, 0.02, 0.9], ax=axes[0])\n", "nlmod.plot.title_inside(\"REGIS\", ax=axes[0])\n", From ae76837a4e68b9f8e8232b9ff66cc79f5cade353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Thu, 4 Jul 2024 11:53:02 +0200 Subject: [PATCH 79/85] Add default CRS EPSG:28992 to GIS functions - suppress pyogrio no CRS warning --- nlmod/gis.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/nlmod/gis.py b/nlmod/gis.py index 3ef06361..2ff5f6b2 100644 --- a/nlmod/gis.py +++ b/nlmod/gis.py @@ -4,13 +4,16 @@ import geopandas as gpd import numpy as np -from .dims.grid import get_affine_mod_to_world, polygons_from_model_ds -from .dims.layers import calculate_thickness +from nlmod.dims.grid import get_affine_mod_to_world, polygons_from_model_ds +from nlmod.dims.layers import calculate_thickness +from nlmod.epsg28992 import EPSG_28992 logger = logging.getLogger(__name__) -def vertex_da_to_gdf(model_ds, data_variables, polygons=None, dealing_with_time="mean"): +def vertex_da_to_gdf( + model_ds, data_variables, polygons=None, dealing_with_time="mean", crs=EPSG_28992 +): """Convert one or more DataArrays from a vertex model dataset to a Geodataframe. Parameters @@ -28,6 +31,9 @@ def vertex_da_to_gdf(model_ds, data_variables, polygons=None, dealing_with_time= becomes very slow. For now only the time averaged data will be saved in the geodataframe. Later this can be extended with multiple possibilities. The default is 'mean'. + crs : str, optional + coordinate reference system for the geodataframe. The default + is EPSG:28992 (RD). Raises ------ @@ -86,7 +92,9 @@ def vertex_da_to_gdf(model_ds, data_variables, polygons=None, dealing_with_time= return gdf -def struc_da_to_gdf(model_ds, data_variables, polygons=None, dealing_with_time="mean"): +def struc_da_to_gdf( + model_ds, data_variables, polygons=None, dealing_with_time="mean", crs=EPSG_28992 +): """Convert one or more DataArrays from a structured model dataset to a Geodataframe. Parameters @@ -99,6 +107,9 @@ def struc_da_to_gdf(model_ds, data_variables, polygons=None, dealing_with_time=" polygons : list of shapely Polygons, optional geometries used for the GeoDataframe, if None the polygons are created from the data variable 'vertices' in model_ds. The default is None. + crs : str, optional + coordinate reference system for the geodataframe. The default + is EPSG:28992 (RD). Raises ------ @@ -152,7 +163,7 @@ def struc_da_to_gdf(model_ds, data_variables, polygons=None, dealing_with_time=" polygons = polygons_from_model_ds(model_ds) # construct geodataframe - gdf = gpd.GeoDataFrame(dv_dic, geometry=polygons) + gdf = gpd.GeoDataFrame(dv_dic, geometry=polygons, crs=crs) return gdf @@ -173,7 +184,6 @@ def dataarray_to_shapefile(model_ds, data_variables, fname, polygons=None): geometries used for the GeoDataframe, if None the polygons are created from the data variable 'vertices' in model_ds. The default is None. - Returns ------- None. @@ -191,6 +201,7 @@ def ds_to_vector_file( driver="GPKG", combine_dic=None, exclude=("x", "y", "time_steps", "area", "vertices", "rch_name", "icvert"), + crs=EPSG_28992, ): """Save all data variables in a model dataset to multiple shapefiles. @@ -213,6 +224,9 @@ def ds_to_vector_file( exclude : tuple of str, optional data variables that are not exported to shapefiles. The default is ('x', 'y', 'time_steps', 'area', 'vertices'). + crs : str, optional + coordinate reference system for the vector file. The default + is EPSG:28992 (RD). Returns ------- @@ -264,9 +278,9 @@ def ds_to_vector_file( for key, item in combine_dic.items(): if set(item).issubset(da_names): if model_ds.gridtype == "structured": - gdf = struc_da_to_gdf(model_ds, item, polygons=polygons) + gdf = struc_da_to_gdf(model_ds, item, polygons=polygons, crs=crs) elif model_ds.gridtype == "vertex": - gdf = vertex_da_to_gdf(model_ds, item, polygons=polygons) + gdf = vertex_da_to_gdf(model_ds, item, polygons=polygons, crs=crs) if driver == "GPKG": gdf.to_file(fname_gpkg, layer=key, driver=driver) else: @@ -282,9 +296,9 @@ def ds_to_vector_file( # create unique shapefiles for the other data variables for da_name in da_names: if model_ds.gridtype == "structured": - gdf = struc_da_to_gdf(model_ds, (da_name,), polygons=polygons) + gdf = struc_da_to_gdf(model_ds, (da_name,), polygons=polygons, crs=crs) elif model_ds.gridtype == "vertex": - gdf = vertex_da_to_gdf(model_ds, (da_name,), polygons=polygons) + gdf = vertex_da_to_gdf(model_ds, (da_name,), polygons=polygons, crs=crs) if driver == "GPKG": gdf.to_file(fname_gpkg, layer=da_name, driver=driver) else: From 41f56052da124a13316d0dd3a80d94557e3879e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Thu, 4 Jul 2024 12:50:30 +0200 Subject: [PATCH 80/85] put back set_coords(index) for flox and suppressing warning --- nlmod/gwf/surface_water.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py index c88ff81b..4128d36e 100644 --- a/nlmod/gwf/surface_water.py +++ b/nlmod/gwf/surface_water.py @@ -922,6 +922,7 @@ def add_min_ahn_to_gdf(gdf, ahn, buffer=0.0, column="ahn_min"): rasterize_function=partial(rasterize_image, all_touched=True), ) gc["ahn"] = ahn + gc = gc.set_coords("index") ahn_min = gc.groupby("index").min()["ahn"].to_pandas() ahn_min.index = ahn_min.index.astype(int) gdf[column] = ahn_min From d9e70498a765ff0d8e88218f590cc8503cc33b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= Date: Thu, 4 Jul 2024 12:56:07 +0200 Subject: [PATCH 81/85] thank you codacy --- nlmod/gis.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nlmod/gis.py b/nlmod/gis.py index 2ff5f6b2..c6739749 100644 --- a/nlmod/gis.py +++ b/nlmod/gis.py @@ -71,7 +71,8 @@ def vertex_da_to_gdf( dv_dic[f"{da_name}_mean"] = da_mean.values else: raise NotImplementedError( - "Can only use the mean of a DataArray with dimension time, use dealing_with_time='mean'" + "Can only use the mean of a DataArray with dimension time, " + "use dealing_with_time='mean'" ) else: raise ValueError( @@ -79,7 +80,8 @@ def vertex_da_to_gdf( ) else: raise NotImplementedError( - f"expected one or two dimensions got {no_dims} for data variable {da_name}" + f"expected one or two dimensions got {no_dims} for " + f"data variable {da_name}" ) # create geometries @@ -87,7 +89,7 @@ def vertex_da_to_gdf( polygons = polygons_from_model_ds(model_ds) # construct geodataframe - gdf = gpd.GeoDataFrame(dv_dic, geometry=polygons) + gdf = gpd.GeoDataFrame(dv_dic, geometry=polygons, crs=crs) return gdf From 31d3c066c4b2203cdee9efbce9248155c41cbcd6 Mon Sep 17 00:00:00 2001 From: Artesia Water Date: Thu, 4 Jul 2024 15:19:45 +0200 Subject: [PATCH 82/85] Fix expired KNMI Data Platform API Test skips were added in #358 because they failed. Co-Authored-By: Martin Vonk <66305055+martinvonk@users.noreply.github.com> --- nlmod/read/knmi_data_platform.py | 35 ++++++++++++++-------------- tests/test_018_knmi_data_platform.py | 12 ++++------ 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/nlmod/read/knmi_data_platform.py b/nlmod/read/knmi_data_platform.py index afda1362..a2b57413 100644 --- a/nlmod/read/knmi_data_platform.py +++ b/nlmod/read/knmi_data_platform.py @@ -21,25 +21,26 @@ def get_anonymous_api_key() -> Union[str, None]: try: - url = "https://developer.dataplatform.knmi.nl/get-started" - tables = read_html(url) # get all tables from url - for table in tables: - for coln in table.columns: - if "KEY" in coln.upper(): # look for columns with key - api_key_str = table.iloc[0].loc[ - coln - ] # get entry with key (first row) - api_key = max( - api_key_str.split(), key=len - ) # get key base on str length - logger.info(f"Retrieved anonymous API Key from {url}") - return api_key + url = "https://developer.dataplatform.knmi.nl/open-data-api#token" + webpage = requests.get(url, timeout=120) # get webpage + api_key = ( + webpage.text.split("")[0].split("
")[-1].strip()
+        )  # obtain apikey from codeblock on webpage
+        if len(api_key) != 120:
+            msg = f"Could not obtain API Key from {url}, trying API Key from memory. Found API Key = {api_key}"
+            logger.error(msg)
+            raise ValueError(msg)
+        logger.info(f"Retrieved anonymous API Key from {url}")
+        return api_key
     except Exception as exc:
-        if Timestamp.today() < Timestamp("2024-07-01"):
-            logger.info("Retrieved anonymous API Key from memory")
+        api_key_memory_date = "2025-07-01"
+        if Timestamp.today() < Timestamp(api_key_memory_date):
+            logger.info(
+                f"Retrieved anonymous API Key (available till {api_key_memory_date}) from memory"
+            )
             api_key = (
-                "eyJvcmciOiI1ZTU1NGUxOTI3NGE5NjAwMDEyYTNlYjEiLCJpZCI6ImE1OGI5"
-                "NGZmMDY5NDRhZDNhZjFkMDBmNDBmNTQyNjBkIiwiaCI6Im11cm11cjEyOCJ9"
+                "eyJvcmciOiI1ZTU1NGUxOTI3NGE5NjAwMDEyYTNlYjEiLCJpZCI6ImE1OGI5N"
+                "GZmMDY5NDRhZDNhZjFkMDBmNDBmNTQyNjBkIiwiaCI6Im11cm11cjEyOCJ9"
             )
             return api_key
         else:
diff --git a/tests/test_018_knmi_data_platform.py b/tests/test_018_knmi_data_platform.py
index db64b8c0..05da19c9 100644
--- a/tests/test_018_knmi_data_platform.py
+++ b/tests/test_018_knmi_data_platform.py
@@ -2,13 +2,11 @@
 import os
 from pathlib import Path
 
-import pytest
-
 from nlmod.read import knmi_data_platform
 
 data_path = Path(__file__).parent / "data"
 
-@pytest.mark.skip(reason="FileNotFoundError: download/INTER_OPER_R___EV24____L3__20240626T000000_20240627T000000_0003.nc not found")
+
 def test_download_multiple_nc_files() -> None:
     dataset_name = "EV24"
     dataset_version = "2"
@@ -22,10 +20,10 @@ def test_download_multiple_nc_files() -> None:
     )
 
     # download the last 10 files
-    fnames = files[-10:]
+    fnames = files[:2]
     dirname = "download"
     knmi_data_platform.download_files(
-        dataset_name, dataset_version, files[-10:], dirname=dirname
+        dataset_name, dataset_version, fnames, dirname=dirname
     )
 
     ds = knmi_data_platform.read_nc(os.path.join(dirname, fnames[0]))
@@ -33,7 +31,7 @@ def test_download_multiple_nc_files() -> None:
     # plot the mean evaporation
     ds["prediction"].mean("time").plot()
 
-@pytest.mark.skip(reason="KeyError: 'files'")
+
 def test_download_read_zip_file() -> None:
     dataset_name = "rad_nl25_rac_mfbs_24h_netcdf4"
     dataset_version = "2.0"
@@ -43,7 +41,7 @@ def test_download_read_zip_file() -> None:
 
     # download the last file
     dirname = "download"
-    fname = files[-1]
+    fname = files[1]
     knmi_data_platform.download_file(
         dataset_name, dataset_version, fname=fname, dirname=dirname
     )

From f8f69539f44ef2ccfea0821f2119eec61b611aa3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= 
Date: Fri, 5 Jul 2024 10:52:24 +0200
Subject: [PATCH 83/85] last fixes - comments onno - new link with info on EPSG
 28992 - add trap for duplicate indices in gdf_to_grid (better error message
 for users)

---
 nlmod/dims/grid.py     |  3 +++
 nlmod/epsg28992.py     | 10 ++++++----
 nlmod/plot/__init__.py |  2 +-
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/nlmod/dims/grid.py b/nlmod/dims/grid.py
index 2b8a3625..df7bf1b9 100644
--- a/nlmod/dims/grid.py
+++ b/nlmod/dims/grid.py
@@ -1706,6 +1706,9 @@ def gdf_to_grid(
     if ml is None and ix is None:
         raise (ValueError("Either specify ml or ix"))
 
+    if gdf.index.has_duplicates or gdf.columns.has_duplicates:
+        raise ValueError("gdf should not have duplicate columns or index.")
+
     if ml is not None:
         if isinstance(ml, xr.Dataset):
             ds = ml
diff --git a/nlmod/epsg28992.py b/nlmod/epsg28992.py
index 665cb1a7..2c65a088 100644
--- a/nlmod/epsg28992.py
+++ b/nlmod/epsg28992.py
@@ -1,8 +1,10 @@
 """
-NOTE: this is the correct epsg:28992 definition for plotting backgroundmaps in RD
-More info (in Dutch) here:
-https://qgis.nl/2011/12/05/epsg28992-of-rijksdriehoekstelsel-verschuiving/
-This was still a problem in October 2023
+NOTE: this is the correct epsg:28992 definition for plotting backgroundmaps in RD.
+
+Related information (in Dutch):
+https://geoforum.nl/t/betrouwbare-bron-voor-proj4-definitie-van-rd-new-epsg-28992/5144/15
+This was still a problem in July 2024.
+
 """
 
 EPSG_28992 = (
diff --git a/nlmod/plot/__init__.py b/nlmod/plot/__init__.py
index e77e6c9b..1563924f 100644
--- a/nlmod/plot/__init__.py
+++ b/nlmod/plot/__init__.py
@@ -8,7 +8,7 @@
     geotop_lithok_in_cross_section,
     geotop_lithok_on_map,
     map_array,
-    # modelextent,
+    modelextent,
     modelgrid,
     surface_water,
 )

From a9cce55caceccf4b46491a821bed8c3e55facb99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dav=C3=ADd=20Brakenhoff?= 
Date: Fri, 5 Jul 2024 12:20:08 +0200
Subject: [PATCH 84/85] accept suggestion onno

---
 .github/workflows/python-publish.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index 0890d7eb..1965aa1f 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -16,7 +16,7 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v5
       with:
-        python-version: '3.9'
+        python-version: '3.11'
 
     - name: Install dependencies
       run: |

From 39c83a5f115926fbd95d10a754a74a30625bd76c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ruben=20Calj=C3=A9?= 
Date: Fri, 5 Jul 2024 12:59:57 +0200
Subject: [PATCH 85/85] Make sure surface water test does not fail for empty
 stages

---
 nlmod/gwf/surface_water.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/nlmod/gwf/surface_water.py b/nlmod/gwf/surface_water.py
index 4128d36e..4d7acd4e 100644
--- a/nlmod/gwf/surface_water.py
+++ b/nlmod/gwf/surface_water.py
@@ -1044,10 +1044,11 @@ def gdf_to_seasonal_pkg(
     # the value to scale is also represented with a time series
     # So we switch the conductance (column 2) and the multiplier (column 3/4)
     spd = np.array(spd, dtype=object)
-    if pkg == "RIV":
-        spd[:, [2, 4]] = spd[:, [4, 2]]
-    else:
-        spd[:, [2, 3]] = spd[:, [3, 2]]
+    if len(spd) > 0:
+        if pkg == "RIV":
+            spd[:, [2, 4]] = spd[:, [4, 2]]
+        else:
+            spd[:, [2, 3]] = spd[:, [3, 2]]
     spd = spd.tolist()
 
     if boundname_column is None: