Skip to content

Commit f76e85b

Browse files
authored
Merge pull request #839 from DHI/derived_variables
Generic derived variables
2 parents 4e13da9 + 192ed04 commit f76e85b

File tree

4 files changed

+223
-4
lines changed

4 files changed

+223
-4
lines changed

docs/examples/Generic.qmd

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,29 @@ ds3 = mikeio.read("gebco_sound_spatial.dfs2")
169169
ds3.Elevation[0].plot();
170170
```
171171

172+
## Derived items
173+
174+
Creating derived items from existing items, e.g. current speed from u and v velocities.
175+
```{python}
176+
from mikeio.generic import DerivedItem
177+
178+
fn = "../data/oresundHD_run1.dfsu"
179+
fn_derived = "oresundHD_speed.dfsu"
180+
mikeio.generic.transform(
181+
fn,
182+
fn_derived,
183+
[
184+
DerivedItem(
185+
name="Current speed",
186+
type=mikeio.EUMType.Current_Speed,
187+
unit=mikeio.EUMUnit.knot,
188+
func=lambda x: 1.94 * np.sqrt(x["U velocity"] ** 2 + x["V velocity"] ** 2),
189+
)
190+
],
191+
)
192+
```
193+
194+
172195
## Time average
173196

174197
```{python}
@@ -211,6 +234,7 @@ da_q75.plot(title="75th percentile, wind speed", label="m/s")
211234

212235
```{python}
213236
import os
237+
214238
os.remove("concat.dfs1")
215239
os.remove("oresundHD_difference.dfsu")
216240
os.remove("extracted.dfs1")
@@ -219,5 +243,6 @@ os.remove("gebco_sound_local_datum.dfs2")
219243
os.remove("gebco_sound_spatial.dfs2")
220244
os.remove("Avg_NorthSea_HD_and_windspeed.dfsu")
221245
os.remove(fn_q)
246+
os.remove("oresundHD_speed.dfsu")
222247
```
223248

mikeio/dfsu/_dfsu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def read(
298298
*,
299299
items: str | int | Sequence[str | int] | None = None,
300300
time: int | str | slice | Sequence[int] | None = None,
301-
elements: Sequence[int] | np.ndarray | None = None,
301+
elements: int | Sequence[int] | np.ndarray | None = None,
302302
area: tuple[float, float, float, float]
303303
| Sequence[tuple[float, float]]
304304
| None = None,

mikeio/generic.py

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from datetime import datetime, timedelta
1111
from shutil import copyfile
1212
from collections.abc import Iterable, Sequence
13-
from typing import Callable, Union
13+
from typing import Callable, Mapping, Union
1414
import warnings
1515

1616

@@ -30,9 +30,10 @@
3030
from mikecore.eum import eumQuantity
3131
from tqdm import tqdm, trange
3232

33+
3334
from . import __dfs_version__
3435
from .dfs._dfs import _get_item_info, _valid_item_numbers
35-
from .eum import ItemInfo
36+
from .eum import ItemInfo, EUMType, EUMUnit
3637
import mikeio
3738

3839

@@ -94,7 +95,7 @@ def _clone(
9495
outfilename: str | pathlib.Path,
9596
start_time: datetime | None = None,
9697
timestep: float | None = None,
97-
items: Sequence[int | DfsDynamicItemInfo] | None = None,
98+
items: Sequence[int | DfsDynamicItemInfo | ItemInfo] | None = None,
9899
datatype: int | None = None,
99100
) -> DfsFile:
100101
source = DfsFileFactory.DfsGenericOpen(str(infilename))
@@ -1011,3 +1012,127 @@ def change_datatype(
10111012

10121013
dfs_out.Close()
10131014
dfs_in.Close()
1015+
1016+
1017+
class DerivedItem:
1018+
"""Item derived from other items.
1019+
1020+
Parameters
1021+
----------
1022+
item: ItemInfo
1023+
ItemInfo object for the derived item
1024+
func: Callable[[Mapping[str, np.ndarray]], np.ndarray] | None
1025+
Function to compute the derived item from a mapping of item names to data arrays.
1026+
If None, the item data will be returned directly from the mapping using the item's name.
1027+
Default is None.
1028+
1029+
Example
1030+
-------
1031+
```{python}
1032+
import numpy as np
1033+
import mikeio
1034+
from mikeio.generic import DerivedItem
1035+
1036+
item = DerivedItem(
1037+
item=ItemInfo("Current Speed", mikeio.EUMType.Current_Speed),
1038+
func=lambda x: np.sqrt(x["U velocity"] ** 2 + x["V velocity"] ** 2),
1039+
)
1040+
1041+
"""
1042+
1043+
def __init__(
1044+
self,
1045+
name: str,
1046+
type: EUMType | None = None,
1047+
unit: EUMUnit | None = None,
1048+
func: Callable[[Mapping[str, np.ndarray]], np.ndarray] | None = None,
1049+
) -> None:
1050+
"""Create a DerivedItem.
1051+
1052+
Parameters
1053+
----------
1054+
name: str
1055+
Name of the derived item.
1056+
type: EUMType
1057+
EUMType of the derived item.
1058+
unit: EUMUnit | None, optional
1059+
EUMUnit of the derived item, pass None to use the default unit for the type.
1060+
Default is None.
1061+
func: Callable[[Mapping[str, np.ndarray]], np.ndarray] | None, optional
1062+
Function to compute the derived item from a mapping of item names to data arrays.
1063+
1064+
"""
1065+
self.item = ItemInfo(name, type, unit)
1066+
self.func = func
1067+
1068+
1069+
def transform(
1070+
infilename: str | pathlib.Path,
1071+
outfilename: str | pathlib.Path,
1072+
vars: Sequence[DerivedItem],
1073+
keep_existing_items: bool = True,
1074+
) -> None:
1075+
"""Transform a dfs file by applying functions to items.
1076+
1077+
Parameters
1078+
----------
1079+
infilename: str | pathlib.Path
1080+
full path to the input file
1081+
outfilename: str | pathlib.Path
1082+
full path to the output file
1083+
vars: Sequence[DerivedItem]
1084+
List of derived items to compute.
1085+
keep_existing_items: bool, optional
1086+
If True, existing items in the input file will be kept in the output file.
1087+
If False, only the derived items will be written to the output file.
1088+
Default is True.
1089+
1090+
"""
1091+
dfs_i = DfsFileFactory.DfsGenericOpen(str(infilename))
1092+
1093+
item_numbers = _valid_item_numbers(dfs_i.ItemInfo)
1094+
n_items = len(item_numbers)
1095+
1096+
items = [v.item for v in vars]
1097+
funcs = {v.item.name: v.func for v in vars}
1098+
1099+
if keep_existing_items:
1100+
existing_items = [
1101+
ItemInfo.from_mikecore_dynamic_item_info(
1102+
dfs_i.ItemInfo[i],
1103+
)
1104+
for i in item_numbers
1105+
]
1106+
items = existing_items + items
1107+
1108+
dfs = _clone(
1109+
str(infilename),
1110+
str(outfilename),
1111+
items=items,
1112+
)
1113+
1114+
n_time_steps = dfs_i.FileInfo.TimeAxis.NumberOfTimeSteps
1115+
1116+
for timestep in range(n_time_steps):
1117+
data = {}
1118+
for item in range(n_items):
1119+
name = dfs_i.ItemInfo[item].Name
1120+
data[name] = dfs_i.ReadItemTimeStep(item_numbers[item] + 1, timestep).Data
1121+
1122+
for item in items:
1123+
func = funcs.get(item.name, None)
1124+
if func is None:
1125+
darray = data[item.name]
1126+
else:
1127+
try:
1128+
darray = func(data)
1129+
except KeyError as e:
1130+
missing_key = e.args[0]
1131+
keys = ", ".join(data.keys())
1132+
raise KeyError(
1133+
f"Item '{missing_key}' is not available in the file. Available items: {keys}"
1134+
)
1135+
dfs.WriteItemTimeStepNext(0.0, darray)
1136+
1137+
dfs_i.Close()
1138+
dfs.Close()

tests/test_generic.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
fill_corrupt,
1313
add,
1414
change_datatype,
15+
transform,
16+
DerivedItem,
1517
)
1618
import pytest
1719
from mikecore.DfsFileFactory import DfsFileFactory
@@ -672,3 +674,70 @@ def test_change_datatype_dfs0(tmp_path: Path) -> None:
672674
org = mikeio.read(infilename).to_numpy()
673675
new = mikeio.read(outfilename).to_numpy()
674676
assert np.allclose(org, new, rtol=1e-08, atol=1e-10, equal_nan=True)
677+
678+
679+
def test_transform_variables(tmp_path: Path) -> None:
680+
infilename = "tests/testdata/oresundHD_run1.dfsu"
681+
outfilename = tmp_path / "need_for_speed.dfsu"
682+
683+
items = [
684+
DerivedItem(
685+
name="Current Speed",
686+
type=mikeio.EUMType.Current_Speed,
687+
func=lambda x: np.sqrt(x["U velocity"] ** 2 + x["V velocity"] ** 2),
688+
)
689+
]
690+
691+
transform(infilename, outfilename, vars=items, keep_existing_items=False)
692+
dfs = mikeio.Dfsu2DH(outfilename)
693+
assert dfs.items[0].type == mikeio.EUMType.Current_Speed
694+
assert len(dfs.items) == 1
695+
696+
dfs1 = mikeio.Dfsu2DH(infilename)
697+
sel_items = [
698+
DerivedItem(name=item.name, type=item.type, unit=item.unit)
699+
for item in dfs1.items
700+
if item.name != "Surface elevation"
701+
]
702+
sel_items.extend(items)
703+
704+
outfilename2 = tmp_path / "existing_and_speed.dfsu"
705+
706+
transform(infilename, outfilename2, vars=sel_items, keep_existing_items=False)
707+
dfs2 = mikeio.Dfsu2DH(outfilename2)
708+
assert dfs2.items[0].name == "Total water depth" # existing item
709+
assert dfs2.items[1].name == "U velocity" # existing item
710+
assert dfs2.items[2].name == "V velocity" # existing item
711+
assert dfs2.items[3].name == "Current Speed" # derived item
712+
713+
714+
def test_transform_can_include_existing_items(tmp_path: Path) -> None:
715+
infilename = "tests/testdata/oresundHD_run1.dfsu"
716+
outfilename = tmp_path / "need_for_speed.dfsu"
717+
718+
items = [
719+
DerivedItem(
720+
name="Current Speed",
721+
type=mikeio.EUMType.Current_Speed,
722+
func=lambda x: np.sqrt(x["U velocity"] ** 2 + x["V velocity"] ** 2),
723+
)
724+
]
725+
726+
transform(infilename, outfilename, vars=items, keep_existing_items=True)
727+
dfs = mikeio.Dfsu2DH(outfilename)
728+
assert dfs.items[-1].type == mikeio.EUMType.Current_Speed
729+
assert len(dfs.items) == 5
730+
ds = dfs.read(time=-1, elements=0)
731+
assert ds["U velocity"].values == pytest.approx(0.0292403083)
732+
assert ds["V velocity"].values == pytest.approx(0.127983957)
733+
assert ds["Current Speed"].values == pytest.approx(0.13128172)
734+
735+
736+
def test_transform_func_with_missing_item_reports_existing_items() -> None:
737+
infilename = "tests/testdata/oresundHD_run1.dfsu"
738+
outfilename = "notgonnahappen.dfsu"
739+
740+
items = [DerivedItem(name="Not relevant", func=lambda x: x["not in the file"])]
741+
with pytest.raises(KeyError) as excinfo:
742+
transform(infilename, outfilename, items)
743+
assert "U velocity" in str(excinfo.value)

0 commit comments

Comments
 (0)