Skip to content

Commit 56070cc

Browse files
authored
Remove model.to_fews, add CF attrs to netcdf input (#2938)
Fixes #2935 I noticed we didn't add any CF metadata to the NetCDF input files, this fixes that and tests it.
1 parent 3326f75 commit 56070cc

File tree

5 files changed

+26
-184
lines changed

5 files changed

+26
-184
lines changed

python/ribasim/ribasim/input_base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,11 +630,25 @@ def _write_netcdf(self, filepath: Path) -> None:
630630
raise err
631631
elif "time" in cols:
632632
ds = self.df.set_index(["time", "node_id"]).to_xarray()
633+
ds["time"].attrs.update(
634+
{"standard_name": "time", "axis": "T", "long_name": "time"}
635+
)
633636
elif "node_id" in cols:
634637
ds = self.df.set_index(["node_id"]).to_xarray()
638+
ds["node_id"].attrs.update(
639+
{"cf_role": "timeseries_id", "long_name": "station identification code"}
640+
)
635641
else:
636642
raise err
637643

644+
ds.attrs.update(
645+
{
646+
"Conventions": "CF-1.12",
647+
"title": "Ribasim model input",
648+
"references": "https://ribasim.org",
649+
}
650+
)
651+
638652
# Write to NetCDF file
639653
ds.to_netcdf(filepath)
640654

python/ribasim/ribasim/model.py

Lines changed: 0 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
)
6464
from ribasim.utils import (
6565
MissingOptionalModule,
66-
_add_cf_attributes,
6766
_concat,
6867
_link_lookup,
6968
_node_lookup,
@@ -895,120 +894,3 @@ def _add_allocation(self, uds):
895894
)
896895

897896
return uds
898-
899-
def to_fews(
900-
self,
901-
region_home: str | PathLike[str],
902-
add_network: bool = True,
903-
add_results: bool = True,
904-
) -> None:
905-
"""
906-
Write the model network and results into files used by Delft-FEWS.
907-
908-
** Warning: This method is experimental and is likely to change. **
909-
910-
To run this method, the model needs to be written to disk, and have results.
911-
The Node, Link and Basin / area tables are written to shapefiles in the REGION_HOME/Config directory.
912-
The results are written to NetCDF files in the REGION_HOME/Modules directory.
913-
The netCDF files are NetCDF4 with CF-conventions.
914-
915-
Parameters
916-
----------
917-
region_home: str | PathLike[str]
918-
Path to the Delft-FEWS REGION_HOME directory.
919-
add_network: bool, optional
920-
Write shapefiles representing the network, enabled by default.
921-
add_results: bool, optional
922-
Write the results to NetCDF files, enabled by default.
923-
"""
924-
region_home = DirectoryPath(region_home)
925-
if add_network:
926-
self._network_to_fews(region_home)
927-
if add_results:
928-
self._results_to_fews(region_home)
929-
930-
def _network_to_fews(self, region_home: DirectoryPath) -> None:
931-
"""Write the Node and Link tables to shapefiles for use in Delft-FEWS."""
932-
df_link = self.link.df
933-
df_node = self.node.df
934-
assert df_link is not None
935-
assert df_node is not None
936-
937-
df_basin_area = self.basin.area.df
938-
if df_basin_area is None:
939-
# Fall back to the Basin points if the area polygons are not set
940-
df_basin_area = df_node[df_node["node_type"] == "Basin"]
941-
942-
network_dir = region_home / "Config/MapLayerFiles/{ModelId}"
943-
network_dir.mkdir(parents=True, exist_ok=True)
944-
link_path = network_dir / "{ModelId}Links.shp"
945-
node_path = network_dir / "{ModelId}Nodes.shp"
946-
basin_area_path = network_dir / "{ModelId}Areas.shp"
947-
948-
with warnings.catch_warnings():
949-
warnings.filterwarnings(
950-
"ignore", "Normalized/laundered field name", RuntimeWarning
951-
)
952-
warnings.filterwarnings(
953-
"ignore",
954-
"Column names longer than 10 characters will be truncated when saved to ESRI Shapefile.",
955-
UserWarning,
956-
)
957-
df_link.to_file(link_path)
958-
df_node.to_file(node_path)
959-
df_basin_area.to_file(basin_area_path)
960-
961-
def _results_to_fews(self, region_home: DirectoryPath) -> None:
962-
"""Convert the model results to NetCDF with CF-conventions for importing into Delft-FEWS."""
963-
# Delft-FEWS doesn't support our UGRID from `model.to_xugrid` yet,
964-
# so we convert Arrow to regular CF-NetCDF4.
965-
966-
ext = self.results_extension
967-
basin_path = self.results_path / f"basin{ext}"
968-
flow_path = self.results_path / f"flow{ext}"
969-
concentration_path = self.results_path / f"concentration{ext}"
970-
971-
if ext == ".arrow":
972-
basin_df = pd.read_feather(basin_path)
973-
flow_df = pd.read_feather(flow_path)
974-
else:
975-
basin_df = xr.open_dataset(basin_path).to_dataframe().reset_index()
976-
flow_df = xr.open_dataset(flow_path).to_dataframe().reset_index()
977-
978-
ds_basin = basin_df.set_index(["time", "node_id"]).to_xarray()
979-
_add_cf_attributes(ds_basin, timeseries_id="node_id")
980-
ds_basin["level"].attrs.update({"units": "m"})
981-
ds_basin["storage"].attrs.update({"units": "m3"})
982-
ds_basin["relative_error"].attrs.update({"units": "1"})
983-
984-
flow_rate_variables = [
985-
"inflow_rate",
986-
"outflow_rate",
987-
"storage_rate",
988-
"precipitation",
989-
"evaporation",
990-
"drainage",
991-
"infiltration",
992-
"balance_error",
993-
]
994-
for var in flow_rate_variables:
995-
ds_basin[var].attrs.update({"units": "m3 s-1"})
996-
997-
ds_flow = flow_df.set_index(["time", "link_id"]).to_xarray()
998-
_add_cf_attributes(ds_flow, timeseries_id="link_id")
999-
ds_flow["flow_rate"].attrs.update({"units": "m3 s-1"})
1000-
1001-
results_dir = region_home / "Modules/ribasim/{ModelId}/work/results"
1002-
results_dir.mkdir(parents=True, exist_ok=True)
1003-
ds_basin.to_netcdf(results_dir / "basin.nc")
1004-
ds_flow.to_netcdf(results_dir / "flow.nc")
1005-
1006-
if concentration_path.is_file():
1007-
if ext == ".arrow":
1008-
df = pd.read_feather(concentration_path)
1009-
else:
1010-
df = xr.open_dataset(concentration_path).to_dataframe().reset_index()
1011-
ds = df.set_index(["time", "node_id", "substance"]).to_xarray()
1012-
_add_cf_attributes(ds, timeseries_id="node_id", realization="substance")
1013-
ds["concentration"].attrs.update({"units": "g m-3"})
1014-
ds.to_netcdf(results_dir / "concentration.nc")

python/ribasim/ribasim/utils.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -90,48 +90,6 @@ def _concat(dfs, **kwargs):
9090
return pd.concat(dfs, **kwargs)
9191

9292

93-
def _add_cf_attributes(ds, timeseries_id: str, realization: str | None = None) -> None:
94-
"""
95-
Add CF attributes to an xarray.Dataset.
96-
97-
Parameters
98-
----------
99-
ds : xarray.Dataset
100-
The dataset to which CF attributes will be added.
101-
timeseries_id : str
102-
The name of the variable that identifies the timeseries.
103-
realization : str | None, optional
104-
The name of the variable representing realizations (e.g., "substance"), if applicable.
105-
106-
Returns
107-
-------
108-
None
109-
"""
110-
ds.attrs.update(
111-
{
112-
"Conventions": "CF-1.12",
113-
"title": "Ribasim model results",
114-
"references": "https://ribasim.org",
115-
}
116-
)
117-
ds["time"].attrs.update({"standard_name": "time", "axis": "T", "long_name": "time"})
118-
ds[timeseries_id].attrs.update(
119-
{"cf_role": "timeseries_id", "long_name": "station identification code"}
120-
)
121-
if realization:
122-
# Use realization as the standard name as recommended by ECMWF.
123-
# axis = "E" is not currently enabled since it seemed to confuse Delft-FEWS.
124-
# https://confluence.ecmwf.int/display/COPSRV/Metadata+recommendations+for+encoding+NetCDF+products+based+on+CF+convention#MetadatarecommendationsforencodingNetCDFproductsbasedonCFconvention-3.4Realizationdiscretecoordinates
125-
ds[realization].attrs.update(
126-
{
127-
"standard_name": "realization",
128-
"units": "1",
129-
"long_name": "substance name",
130-
}
131-
)
132-
return ds
133-
134-
13593
class UsedIDs(BaseModel):
13694
"""A helper class to manage globally unique node IDs.
13795

python/ribasim/tests/test_io.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import pytest
99
import ribasim
1010
import tomli
11+
import xarray as xr
1112
from pandas import DataFrame
1213
from pandas.testing import assert_frame_equal
1314
from pydantic import ValidationError
@@ -387,7 +388,17 @@ def test_roundtrip_netcdf(tabulated_rating_curve_control, tmp_path):
387388
model2 = Model.read(model1dir / "ribasim.toml")
388389

389390
assert (model1dir / "input/database.gpkg").is_file()
390-
assert (model1dir / "input/basin-state.nc").is_file()
391+
basin_state_path = model1dir / "input/basin-state.nc"
392+
assert basin_state_path.is_file()
393+
394+
# check CF attributes in the NetCDF file
395+
with xr.open_dataset(basin_state_path) as ds:
396+
assert ds.attrs["Conventions"] == "CF-1.12"
397+
assert ds.attrs["title"] == "Ribasim model input"
398+
assert ds.attrs["references"] == "https://ribasim.org"
399+
node_attrs = ds["node_id"].attrs
400+
assert node_attrs["cf_role"] == "timeseries_id"
401+
assert node_attrs["long_name"] == "station identification code"
391402

392403
df1 = model1.basin.state.df
393404
df2 = model2.basin.state.df

python/ribasim/tests/test_model.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -213,29 +213,6 @@ def test_to_xugrid(model, tmp_path):
213213
model.to_xugrid(add_flow=True, add_allocation=True)
214214

215215

216-
@pytest.mark.parametrize(
217-
"model",
218-
[basic_model(), outlet_model(), pid_control_equation_model(), trivial_model()],
219-
)
220-
def test_to_fews(model, tmp_path):
221-
region_home = tmp_path
222-
network_dir = region_home / "Config/MapLayerFiles/{ModelId}"
223-
224-
with pytest.raises(FileNotFoundError, match="Model must be written to disk"):
225-
model.to_fews(region_home)
226-
227-
model.write(tmp_path / "model/ribasim.toml")
228-
model.to_fews(region_home, add_results=False)
229-
assert (network_dir / "{ModelId}Links.dbf").is_file()
230-
assert (network_dir / "{ModelId}Links.shp").is_file()
231-
assert (network_dir / "{ModelId}Nodes.dbf").is_file()
232-
assert (network_dir / "{ModelId}Nodes.shp").is_file()
233-
234-
# Cannot test results=True without results
235-
with pytest.raises(FileNotFoundError, match=r"Cannot find basin_state.nc"):
236-
model.to_fews(region_home, add_results=True)
237-
238-
239216
def test_to_crs(bucket: Model):
240217
model = bucket
241218

0 commit comments

Comments
 (0)