nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: DHI/mikeio
Pull request #562 (2023-03-29 15:15:07)

Dtype dfs3 read
Allow specification of dtype for Dfs3.read() (as implemented in Dfs2.read() )
As described in #558 


Patch:
diff --git a/mikeio/dfs/_dfs2.py b/mikeio/dfs/_dfs2.py
index 4d456d43..790961a0 100644
--- a/mikeio/dfs/_dfs2.py
+++ b/mikeio/dfs/_dfs2.py
@@ -200,7 +200,8 @@ class Dfs2(_Dfs123):
         area: array[float], optional
             Read only data inside (horizontal) area given as a
             bounding box (tuple with left, lower, right, upper) coordinates
-
+        dtype: data-type, optional
+            Define the dtype of the returned dataset (default = np.float32)
         Returns
         -------
         Dataset
diff --git a/mikeio/dfs/_dfs3.py b/mikeio/dfs/_dfs3.py
index 56de390b..9bb04f9c 100644
--- a/mikeio/dfs/_dfs3.py
+++ b/mikeio/dfs/_dfs3.py
@@ -188,6 +188,7 @@ class Dfs3(_Dfs123):
         area=None,
         layers=None,
         keepdims=False,
+        dtype=np.float32,
     ) -> Dataset:
         """
         Read data from a dfs3 file
@@ -204,6 +205,8 @@ class Dfs3(_Dfs123):
             in the returned Dataset? by default: False
         layers: int, str, list[int], optional
             Read only data for specific layers, by default None
+        dtype: data-type, optional
+            Define the dtype of the returned dataset (default = np.float32)
 
         Returns
         -------
@@ -250,7 +253,7 @@ class Dfs3(_Dfs123):
             shape = (nt, nz, yNum, xNum)
 
         for item in range(n_items):
-            data = np.ndarray(shape=shape, dtype=float)
+            data = np.ndarray(shape=shape, dtype=dtype)
             data_list.append(data)
 
         if single_time_selected and not keepdims:


Test patch:
diff --git a/tests/test_dfs3.py b/tests/test_dfs3.py
index acd63149..2ccf8a18 100644
--- a/tests/test_dfs3.py
+++ b/tests/test_dfs3.py
@@ -1,5 +1,6 @@
 import os
 import pytest
+import numpy as np
 
 import mikeio
 from mikeio.spatial import GeometryUndefined
@@ -46,6 +47,12 @@ def test_dfs3_read():
     assert da.shape == (30, 10, 10, 10)  # t  # z  # y  # x
     assert da.dims == ("time", "z", "y", "x")
     assert da.name == "Item 1"
+    assert da.to_numpy().dtype == np.float32
+
+
+def test_dfs3_read_double_precision():
+    ds = mikeio.read("tests/testdata/Grid1.dfs3", dtype=np.float64)
+    assert ds[0].to_numpy().dtype == np.float64
 
 
 def test_dfs3_read_time():
Repository: DHI/mikeio
Pull request #567 (2023-04-27 14:55:46)

Validate projection string
In the spirit of failing fast, validate that a projection string is acceptable by MIKE on creation of geometry instead on writing to disk.


Patch:
diff --git a/mikeio/spatial/_geometry.py b/mikeio/spatial/_geometry.py
index 079ca735..56ab92b3 100644
--- a/mikeio/spatial/_geometry.py
+++ b/mikeio/spatial/_geometry.py
@@ -2,11 +2,18 @@ from abc import ABC, abstractmethod
 
 from collections import namedtuple
 
+from mikecore.Projections import MapProjection
+
 BoundingBox = namedtuple("BoundingBox", ["left", "bottom", "right", "top"])
 
 
 class _Geometry(ABC):
     def __init__(self, projection=None) -> None:
+
+        # TODO should projection be a mandatory argument?
+        if projection and not MapProjection.IsValid(projection):
+            raise ValueError(f"{projection=} is not a valid projection string")
+
         self._projstr = projection if projection else "LONG/LAT"
 
     @property


Test patch:
diff --git a/tests/test_geometry_grid.py b/tests/test_geometry_grid.py
index 84941407..4ae2045b 100644
--- a/tests/test_geometry_grid.py
+++ b/tests/test_geometry_grid.py
@@ -426,3 +426,9 @@ def test_grid2d_equality():
     g6 = Grid2D(dx=0.1, nx=2, dy=0.2, ny=4, y0=5)
 
     assert g5 != g6
+
+
+def test_bad_projection_raises_error():
+
+    with pytest.raises(ValueError, match="proj"):
+        Grid2D(nx=2, ny=2, dx=0.1, projection="Not a WKT projection string")
Repository: DHI/mikeio
Pull request #575 (2023-06-08 16:50:55)

Pfs: Handle CLOB on write


Patch:
diff --git a/mikeio/pfs/_pfssection.py b/mikeio/pfs/_pfssection.py
index ea2009da..43b426b8 100644
--- a/mikeio/pfs/_pfssection.py
+++ b/mikeio/pfs/_pfssection.py
@@ -358,7 +358,7 @@ class PfsSection(SimpleNamespace, MutableMapping):
             if v == "":
                 # add either '' or || as pre- and suffix to strings depending on path definition
                 v = "''"
-            elif v.count("|") == 2:
+            elif v.count("|") == 2 and "CLOB" not in v:
                 v = f"{v}"
             else:
                 v = f"'{v}'"


Test patch:
diff --git a/tests/test_pfs.py b/tests/test_pfs.py
index 51b22ebf..87a5494c 100644
--- a/tests/test_pfs.py
+++ b/tests/test_pfs.py
@@ -1208,5 +1208,28 @@ def test_clob_can_contain_pipe_characters():
     assert sct.TemplateFile == "||"
     assert (
         sct.Clob
-        == r'<CLOB:22,1,1,false,1,0,"",0,"",0,"",0,"",0,"",0,"",0,"",0,"",||,false>'
+        == '<CLOB:22,1,1,false,1,0,"",0,"",0,"",0,"",0,"",0,"",0,"",0,"",||,false>'
     )
+
+def test_write_read_clob(tmp_path):
+    clob_text = """
+    [WQRiverPfs_0]
+        Clob = '<CLOB:22,1,1,false,1,0,"",0,"",0,"",0,"",0,"",0,"",0,"",0,"",||,false>'
+    EndSect  // WQRiverPfs_0
+   """
+    pfs = mikeio.PfsDocument(StringIO(clob_text))
+    sct = pfs.WQRiverPfs_0
+    assert (
+        sct.Clob
+        == '<CLOB:22,1,1,false,1,0,"",0,"",0,"",0,"",0,"",0,"",0,"",0,"",||,false>'
+    )
+
+    fp = tmp_path / "clob.pfs"
+    pfs.write(fp)
+
+    pfs2 = mikeio.PfsDocument(fp)
+    sct = pfs2.WQRiverPfs_0
+    assert (
+        sct.Clob
+        == '<CLOB:22,1,1,false,1,0,"",0,"",0,"",0,"",0,"",0,"",0,"",0,"",||,false>'
+    )
\ No newline at end of file
Repository: DHI/mikeio
Pull request #590 (2023-08-07 06:45:24)

Don't write non-equidistant dfsu
And certainly not with a wrong timestep.


Patch:
diff --git a/mikeio/dfsu/_dfsu.py b/mikeio/dfsu/_dfsu.py
index 19ecd9d7..b875ccf8 100644
--- a/mikeio/dfsu/_dfsu.py
+++ b/mikeio/dfsu/_dfsu.py
@@ -45,6 +45,9 @@ def _write_dfsu(filename: str, data: Dataset):
     if len(data.time) == 1:
         dt = 1  # TODO is there any sensible default?
     else:
+        if not data.is_equidistant:
+            raise ValueError("Non-equidistant time axis is not supported.")
+
         dt = (data.time[1] - data.time[0]).total_seconds()  # type: ignore
     n_time_steps = len(data.time)
 


Test patch:
diff --git a/tests/test_dfsu.py b/tests/test_dfsu.py
index f4850c28..d4e8b5a0 100644
--- a/tests/test_dfsu.py
+++ b/tests/test_dfsu.py
@@ -1166,3 +1166,16 @@ def test_write_header(tmp_path):
                 d = np.random.random((n_elements))  # 1d
                 data.append(d)
                 f.append(data)
+
+def test_writing_non_equdistant_dfsu_is_not_possible(tmp_path):
+
+    ds = mikeio.read("tests/testdata/wind_north_sea.dfsu")
+    dss = ds.isel(time=[0,2,3])
+    assert not dss.is_equidistant
+
+    # TODO which exception should be raised, when trying to write non-equidistant dfsu? This operation is not supported
+    with pytest.raises(ValueError, match="equidistant"):
+        fp = tmp_path / "not_gonna_work.dfsu"
+        dss.to_dfs(fp)
+
+    
\ No newline at end of file
Repository: DHI/mikeio
Pull request #652 (2024-02-20 07:56:31)

Concat neq dfs0


Patch:
diff --git a/mikeio/generic.py b/mikeio/generic.py
index be9ab9b0..df3cf194 100644
--- a/mikeio/generic.py
+++ b/mikeio/generic.py
@@ -27,6 +27,7 @@ from tqdm import tqdm, trange
 from . import __dfs_version__
 from .dfs._dfs import _get_item_info, _valid_item_numbers
 from .eum import ItemInfo
+import mikeio
 
 
 TimeAxis = Union[
@@ -449,6 +450,13 @@ def concat(
 
     The list of input files have to be sorted, i.e. in chronological order
     """
+    # fast path for Dfs0
+    suffix = pathlib.Path(infilenames[0]).suffix
+    if suffix == ".dfs0":
+        dss = [mikeio.read(f) for f in infilenames]
+        ds = mikeio.Dataset.concat(dss, keep=keep)  # type: ignore
+        ds.to_dfs(outfilename)
+        return
 
     dfs_i_a = DfsFileFactory.DfsGenericOpen(str(infilenames[0]))
 


Test patch:
diff --git a/tests/test_generic.py b/tests/test_generic.py
index 02fda0bc..aa311292 100644
--- a/tests/test_generic.py
+++ b/tests/test_generic.py
@@ -318,6 +318,39 @@ def test_concat_keep(tmp_path):
                     assert last_out, "overlap should be with last dataset"
 
 
+def test_concat_non_equidistant_dfs0(tmp_path):
+
+    # create two non-equidistant dfs0 files
+    da1 = mikeio.DataArray(
+        data=np.array([0.0, 0.1, 0.2]),
+        time=pd.DatetimeIndex(
+            ["2017-10-27 01:58", "2017-10-27 04:32", "2017-10-27 05:32"]
+        ),
+    )
+    assert not da1.is_equidistant
+    da2 = mikeio.DataArray(
+        data=np.array([0.9, 1.1, 0.2]),
+        time=pd.DatetimeIndex(
+            ["2017-10-28 01:59", "2017-10-29 04:32", "2017-11-01 05:32"]
+        ),
+    )
+    assert not da2.is_equidistant
+    files = [tmp_path / "da1.dfs0", tmp_path / "da2.dfs0"]
+    da1.to_dfs(tmp_path / "da1.dfs0")
+    da2.to_dfs(tmp_path / "da2.dfs0")
+
+    # concatenate
+    fp = tmp_path / "concat.dfs0"
+    mikeio.generic.concat(files, fp)
+
+    ds = mikeio.read(fp)
+    assert len(ds.time) == 6
+
+    assert ds.sel(time="2017-10-29 04:32").to_dataframe().iloc[0, 0] == pytest.approx(
+        1.1
+    )
+
+
 def test_extract_equidistant(tmp_path):
 
     infile = "tests/testdata/waves.dfs2"
Repository: DHI/mikeio
Pull request #787 (2025-02-04 07:46:00)

Allow modification of unit


Patch:
diff --git a/mikeio/dataset/_dataarray.py b/mikeio/dataset/_dataarray.py
index fb46ddbd..28986a19 100644
--- a/mikeio/dataset/_dataarray.py
+++ b/mikeio/dataset/_dataarray.py
@@ -407,7 +407,11 @@ class DataArray:
     @property
     def unit(self) -> EUMUnit:
         """EUMUnit."""
-        return self.item.unit
+        return self.item._unit
+
+    @unit.setter
+    def unit(self, value: EUMUnit) -> None:
+        self.item.unit = value
 
     @property
     def start_time(self) -> datetime:
diff --git a/mikeio/eum/_eum.py b/mikeio/eum/_eum.py
index 96d9ea2f..7652dd1d 100644
--- a/mikeio/eum/_eum.py
+++ b/mikeio/eum/_eum.py
@@ -1465,12 +1465,12 @@ class ItemInfo:
                 raise ValueError(
                     "Invalid unit. Unit should be supplied as EUMUnit, e.g. ItemInfo('WL',EUMType.Water_Level, EUMUnit.meter)"
                 )
-            self.unit = unit
+            self._unit = unit
         else:
             if self.type == EUMType.Undefined:
-                self.unit = EUMUnit.undefined
+                self._unit = EUMUnit.undefined
             else:
-                self.unit = self.type.units[0]
+                self._unit = self.type.units[0]
 
         self.data_value_type = to_datatype(data_value_type)
 
@@ -1496,6 +1496,20 @@ class ItemInfo:
         else:
             return f"{self.name} <{self.type.display_name}> ({self.unit.display_name}) - {self.data_value_type}"
 
+    @property
+    def unit(self) -> EUMUnit:
+        "Item unit."
+        return self._unit
+
+    @unit.setter
+    def unit(self, value: EUMUnit) -> None:
+        "Set unit."
+        if value not in self.type.units:
+            raise ValueError(
+                f"{value} is not a correct unit for {self.type}. Use {self.type.units}"
+            )
+        self._unit = value
+
     @staticmethod
     def from_mikecore_dynamic_item_info(dfsItemInfo: DfsDynamicItemInfo) -> "ItemInfo":
         """Create ItemInfo from a mikecore.DfsDynamicItemInfo object."""


Test patch:
diff --git a/tests/test_dataarray.py b/tests/test_dataarray.py
index b1a81cba..53ead4d0 100644
--- a/tests/test_dataarray.py
+++ b/tests/test_dataarray.py
@@ -1376,3 +1376,24 @@ def test_set_by_mask():
     mask = da < threshold
     wl_capped = da.copy()
     wl_capped[mask] = np.nan
+
+
+def test_set_unit() -> None:
+    da = mikeio.DataArray(
+        data=np.array([0.0, 1.0]),
+        item=mikeio.ItemInfo("Water", mikeio.EUMType.Water_Level, mikeio.EUMUnit.meter),
+    )
+
+    da.unit = mikeio.EUMUnit.feet
+
+    assert da.unit == mikeio.EUMUnit.feet
+
+
+def test_set_bad_unit_fails() -> None:
+    da = mikeio.DataArray(
+        data=np.array([0.0, 1.0]),
+        item=mikeio.ItemInfo("Water", mikeio.EUMType.Water_Level, mikeio.EUMUnit.meter),
+    )
+
+    with pytest.raises(ValueError, match="unit"):
+        da.unit = mikeio.EUMUnit.decibar