Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ __pycache__

.ruff-cache
.venv
.vscode/settings.json
36 changes: 17 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ and produce a hydrograph.
In condensed code:

```python
forcing = ewatercycle.forcing.sources['MarrmotForcing'].generate(...)
model = ewatercycle.models.sources['MarrmotM14'](forcing)
forcing = ewatercycle.forcing.sources["MarrmotForcing"].generate(...)
model = ewatercycle.models.sources["MarrmotM14"](forcing)
model.setup(...)
model.initialize()
while (model.time < model.end_time):
while model.time < model.end_time:
model.update()
value = model.get_value_as_xarray('flux_out_Q')
value = model.get_value_as_xarray("flux_out_Q")
model.finalize()
ewatercycle.analysis.hydrograph(...)
```
Expand All @@ -98,14 +98,14 @@ from ewatercycle.testing.fixtures import rhine_shape
import shapefile
import xarray as xr

forcing = ewatercycle.forcing.sources['MarrmotForcing'].generate(
dataset='ERA5',
start_time='2010-01-01T00:00:00Z',
end_time='2010-12-31T00:00:00Z',
shape=rhine_shape()
forcing = ewatercycle.forcing.sources["MarrmotForcing"].generate(
dataset="ERA5",
start_time="2010-01-01T00:00:00Z",
end_time="2010-12-31T00:00:00Z",
shape=rhine_shape(),
)

model = ewatercycle.models.sources['MarrmotM14'](version='2020.11', forcing=forcing)
model = ewatercycle.models.sources["MarrmotM14"](version="2020.11", forcing=forcing)

cfg_file, cfg_dir = model.setup(
threshold_flow_generation_evap_change=0.1,
Expand All @@ -115,31 +115,29 @@ model.initialize(cfg_file)

# flux_out_Q unit conversion factor from mm/day to m3/s
sf = shapefile.Reader(rhine_shape())
area = sf.record(0)['SUB_AREA'] * 1e6 # from shapefile in m2
area = sf.record(0)["SUB_AREA"] * 1e6 # from shapefile in m2
conversion_mmday2m3s = 1 / (1000 * 24 * 60 * 60)
conversion = conversion_mmday2m3s * area

simulated_discharge = []
while (model.time < model.end_time):
while model.time < model.end_time:
model.update()
simulated_discharge.append(
model.get_value_as_xarray('flux_out_Q')
)
simulated_discharge.append(model.get_value_as_xarray("flux_out_Q"))

observations_ds = ewatercycle.observation.grdc.get_grdc_data(
station_id=6335020, # Rees, Germany
start_time=model.start_time_as_isostr,
end_time=model.end_time_as_isostr,
column='observation',
column="observation",
)

# Combine the simulated discharge with the observations
sim_da = xr.concat(simulated_discharge, dim='time') * conversion
sim_da.name = 'simulated'
sim_da = xr.concat(simulated_discharge, dim="time") * conversion
sim_da.name = "simulated"
discharge = xr.merge([sim_da, observations_ds["observation"]]).to_dataframe()
discharge = discharge[["observation", "simulated"]].dropna()

ewatercycle.analysis.hydrograph(discharge, reference='observation')
ewatercycle.analysis.hydrograph(discharge, reference="observation")

model.finalize()
```
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies = [
"xarray",
"fsspec",
"cartopy",
"HydroErr==2.0.0",
# Pin pyopenssl (conda installs old version) https://github.com/conda/conda/issues/13619
"pyopenssl>=24.0.0",
]
Expand Down
2 changes: 1 addition & 1 deletion src/ewatercycle/_forcings/caravan.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def generate( # type: ignore[override]
raise ValueError(msg)
basin_id = str(kwargs["basin_id"])

dataset: str = basin_id.split("_")[0]
dataset: str = basin_id.split("_", maxsplit=1)[0]
ds = cls.get_dataset(dataset)
ds_basin = ds.sel(basin_id=basin_id.encode())
ds_basin_time = crop_ds(ds_basin, start_time, end_time)
Expand Down
160 changes: 3 additions & 157 deletions src/ewatercycle/analysis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,159 +1,5 @@
"""Analysis methods for eWaterCycle."""
"""ewatercycle analysis module."""

import os
from ewatercycle.analysis import hydrograph

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from hydrostats import metrics
from matplotlib.axes import Axes
from matplotlib.dates import DateFormatter
from matplotlib.figure import Figure


def _downsample(df, nrows=100, agg="mean"):
"""Resample dataframe with datetimeindex to a fixed number of rows."""
if len(df) <= nrows:
return df, df.index[1] - df.index[0]

grouper = np.arange(len(df)) // (len(df) / nrows)
new_df = df.groupby(grouper).agg(agg)
new_df.index = pd.date_range(df.index[0], df.index[-1], periods=nrows)

new_period = (df.index[-1] - df.index[0]) / nrows

return new_df, new_period


def hydrograph(
discharge: pd.DataFrame,
*,
reference: str,
precipitation: pd.DataFrame | None = None,
dpi: int | None = None,
title: str = "Hydrograph",
discharge_units: str = "m$^3$ s$^{-1}$",
precipitation_units: str = "mm day$^{-1}$",
figsize: tuple[float, float] = (10, 10),
filename: os.PathLike | str | None = None,
nbars: int | None = None,
**kwargs,
) -> tuple[Figure, tuple[Axes, Axes]]:
"""Plot a hydrograph.

This utility function makes it convenient to create a hydrograph from
a set of discharge data from a `pandas.DataFrame`. A column must be marked
as the reference, so that the agreement metrics can be calculated.

Optionally, the corresponding precipitation data can be plotted for
comparison.

Args:
discharge: Dataframe containing time series of discharge data to be plotted.
reference: Name of the reference data, must correspond to a column
in the discharge dataframe. Metrics are calculated between
the reference column and each of the other columns.
precipitation: Optional dataframe containing time series of precipitation data
to be plotted from the top of the hydrograph.
dpi: DPI for the plot.
title: Title of the hydrograph.
discharge_units: Units for the discharge data.
precipitation_units: Units for the precipitation data.
figsize: With, height of the plot in inches.
filename: If specified, a copy of the plot will be saved to this path.
nbars: Number of bars to use for downsampling precipitation.
**kwargs: Options to pass to the matplotlib plotting function

Returns:
First tuple member is a matplotlib figure, the second is a tuple of axes.
"""
discharge_cols = discharge.columns.drop(reference)
y_obs = discharge[reference]
y_sim = discharge[discharge_cols]

fig, (ax, ax_tbl) = plt.subplots(
nrows=2,
ncols=1,
dpi=dpi,
figsize=figsize,
gridspec_kw={"height_ratios": [3, 1]},
)

ax.set_title(title)
ax.set_ylabel(f"Discharge ({discharge_units})")

y_sim.plot(ax=ax, **kwargs)
y_obs.plot(ax=ax, **kwargs)

handles, labels = ax.get_legend_handles_labels()

# Add precipitation as bar plot to the top if specified
if precipitation is not None:
if nbars is not None:
precipitation, barwidth = _downsample(
precipitation, nrows=nbars, agg="mean"
)
else:
barwidth = 0.8 # default value for matplotlib barplot

ax_pr = ax.twinx()
ax_pr.invert_yaxis()
ax_pr.set_ylabel(f"Precipitation ({precipitation_units})")

for pr_label, pr_timeseries in precipitation.items():
ax_pr.bar(
pr_timeseries.index.values,
pr_timeseries.values,
width=barwidth,
alpha=0.4,
label=pr_label,
)

# tweak ylim to make space at bottom and top
ax_pr.set_ylim(ax_pr.get_ylim()[0] * (7 / 2), 0)
ax.set_ylim(0, ax.get_ylim()[1] * (7 / 5))

# prepend handles/labels so they appear at the top
handles_pr, labels_pr = ax_pr.get_legend_handles_labels()
handles = handles_pr + handles
labels = labels_pr + labels

# Put the legend outside the plot
ax.legend(handles, labels, bbox_to_anchor=(1.10, 1), loc="upper left")

# set formatting for xticks
date_fmt = DateFormatter("%Y-%m")
ax.xaxis.set_major_formatter(date_fmt)
ax.tick_params(axis="x", rotation=30)

# calculate metrics for data table underneath plot
def calc_metric(metric) -> float:
return y_sim.apply(metric, observed_array=y_obs)

metrs = pd.DataFrame(
{
"nse": calc_metric(metrics.nse),
"kge_2009": calc_metric(metrics.kge_2009),
"sa": calc_metric(metrics.sa),
"me": calc_metric(metrics.me),
}
)

# convert data in dataframe to strings
cell_text = [[f"{item:.2f}" for item in row[1]] for row in metrs.iterrows()]

table = ax_tbl.table(
cellText=cell_text,
rowLabels=metrs.index,
colLabels=metrs.columns,
loc="center",
)
ax_tbl.set_axis_off()

# give more vertical space in cells
table.scale(1, 1.5)

if filename is not None:
fig.savefig(filename, bbox_inches="tight", dpi=dpi)

return fig, (ax, ax_tbl)
__all__ = ["hydrograph"]
Loading
Loading