From 0c0e544ded4a816e0a9f2218d3cf478c4a41febd Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:03:08 +0200 Subject: [PATCH 1/7] Test that each factory axis is written and read back with its unit DfsFactory.CreateAxisDummy builds its axis with a bare 0 as the unit, so writing an item on it raises AttributeError ('int' object has no attribute 'value'). The test writes an item on every axis the factory can write (Dummy, EqD0 to EqD3), reads the file back through the native reader and checks the axis type, unit and data. Dummy fails until the next commit. Co-Authored-By: Claude Opus 5.5 --- tests/test_dfsbuilder.py | 53 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_dfsbuilder.py b/tests/test_dfsbuilder.py index 681b77f..7e518a2 100644 --- a/tests/test_dfsbuilder.py +++ b/tests/test_dfsbuilder.py @@ -8,6 +8,7 @@ DfsStaticItemBuilder, ) from mikecore.DfsFactory import DfsFactory +from mikecore.DfsFile import SpaceAxisType from mikecore.DfsFileFactory import DfsFileFactory from mikecore.eum import eumUnit, eumQuantity, eumItem @@ -243,6 +244,58 @@ def test_validate_ignores_encode_key_of_dfs0(tmp_path): assert item.Data.tolist() == [7] +@pytest.mark.parametrize( + "create, axisType, axisUnit, size", + [ + ( + lambda f: f.CreateAxisDummy(5), + SpaceAxisType.EqD1, + eumUnit.eumUUnitUndefined, + 5, + ), + (lambda f: f.CreateAxisEqD0(), SpaceAxisType.EqD0, eumUnit.eumUmeter, 1), + ( + lambda f: f.CreateAxisEqD1(eumUnit.eumUfeet, 5, 0, 1), + SpaceAxisType.EqD1, + eumUnit.eumUfeet, + 5, + ), + ( + lambda f: f.CreateAxisEqD2(eumUnit.eumUkilometer, 4, 0, 1, 3, 0, 1), + SpaceAxisType.EqD2, + eumUnit.eumUkilometer, + 12, + ), + ( + lambda f: f.CreateAxisEqD3(eumUnit.eumUmeter, 4, 0, 1, 3, 0, 1, 2, 0, 1), + SpaceAxisType.EqD3, + eumUnit.eumUmeter, + 24, + ), + ], + ids=["Dummy", "EqD0", "EqD1", "EqD2", "EqD3"], +) +def test_factory_axis_is_written_and_read_back( + tmp_path, create, axisType, axisUnit, size +): + builder = compressed_builder_for(create(DfsFactory())) + filename = str(tmp_path / "axis.dfs") + builder.CreateFile(filename) + dfs = builder.GetFile() + dfs.WriteItemTimeStepNext(0, np.arange(size, dtype=np.float32)) + dfs.Close() + + dfs = DfsFileFactory.DfsGenericOpen(filename) + axis = dfs.ItemInfo[0].SpatialAxis + item = dfs.ReadItemTimeStepNext() + dfs.Close() + + assert axisType == axis.AxisType + assert axisUnit == axis.AxisUnit + assert item is not None + assert item.Data.tolist() == list(range(size)) + + def test_static_item_builder_reports_missing_data_and_axis(): errors = DfsStaticItemBuilder().Validate() From 9012a4c01da00efedef008f928b0670ec34b3452 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:03:16 +0200 Subject: [PATCH 2/7] Give CreateAxisDummy an eumUnit instead of a bare 0 It now returns DfsAxisEqD1.CreateDummyAxis, the undefined-unit axis that DfsStaticItem.Create already gives a static item created without an axis, so writing an item on it no longer raises AttributeError. Co-Authored-By: Claude Opus 5.5 --- mikecore/DfsFactory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mikecore/DfsFactory.py b/mikecore/DfsFactory.py index 9a096c4..5b4b4ca 100644 --- a/mikecore/DfsFactory.py +++ b/mikecore/DfsFactory.py @@ -58,7 +58,7 @@ def CreateCustomBlock(self, name, data): # region Spatial axis factory methods def CreateAxisDummy(self, numberOfValues): - return DfsAxisEqD1(0, numberOfValues, 0, 1) + return DfsAxisEqD1.CreateDummyAxis(numberOfValues) def CreateAxisEqD0(self): return DfsAxisEqD0() From 0476ac95de5c42982e810444bd48555f34ad271e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:03:58 +0200 Subject: [PATCH 3/7] Restore the static-item axis unit assertions from the C# tests The C# suite asserts eumUmeter, which holds for engine-written files but not for files mikecore builds: their static items get the undefined-unit dummy axis. The Oresund StaticItemTester now takes an original flag and asserts the exact unit for each, the way test_dfsu_file.py already does, and a new test_ReadOresundHDTest runs the testers on the engine-written OresundHD.dfsu so the eumUmeter branch runs. test_dfs_static_item reads that file, so it asserts eumUmeter. Co-Authored-By: Claude Opus 5.5 --- tests/test_dfs_static_item.py | 2 +- tests/test_dfsu2D.py | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/test_dfs_static_item.py b/tests/test_dfs_static_item.py index 5217b67..cf62992 100644 --- a/tests/test_dfs_static_item.py +++ b/tests/test_dfs_static_item.py @@ -113,7 +113,7 @@ def test_static_item(self): axis = staticItem.SpatialAxis assert SpaceAxisType.EqD1 == axis.AxisType assert 1 == axis.Dimension - # TODO: Assert.AreEqual(eumUnit.eumUmeter, axis.AxisUnit + assert eumUnit.eumUmeter == axis.AxisUnit assert 3 * 3636 == axis.XCount assert 0 == axis.X0 assert 1 == axis.Dx diff --git a/tests/test_dfsu2D.py b/tests/test_dfsu2D.py index 5747cd9..5f80270 100644 --- a/tests/test_dfsu2D.py +++ b/tests/test_dfsu2D.py @@ -71,6 +71,17 @@ def test_CreateOresundHDTest(self): FileOresundHDDfsu.StaticItemTester(dfsFile) FileOresundHDDfsu.ReadTester(dfsFile) + def test_ReadOresundHDTest(self): + dfsFile = DfsFileFactory.DfsGenericOpen("testdata/OresundHD.dfsu") + + FileOresundHDDfsu.FileInfoTester(dfsFile) + FileOresundHDDfsu.CustomBlockTester(dfsFile) + FileOresundHDDfsu.DynamicItemTester(dfsFile) + FileOresundHDDfsu.StaticItemTester(dfsFile, original=True) + FileOresundHDDfsu.ReadTester(dfsFile) + + dfsFile.Close() + def test_ReadOdenseHD2DTest(self): filename = "testdata/OdenseHD2D.dfsu" @@ -578,7 +589,7 @@ def DynamicItemTester(dfsFile): Assert.AreEqual(1, axis.Dx) @staticmethod - def StaticItemTester(dfsFile): + def StaticItemTester(dfsFile, original=False): staticItems = [] staticItemNumber = 1 @@ -638,7 +649,10 @@ def StaticItemTester(dfsFile): axis = staticItem.SpatialAxis Assert.IsNotNull(axis) Assert.AreEqual(1, axis.Dimension) - # TODO: Assert.AreEqual(eumUnit.eumUmeter, axis.AxisUnit); + if original: + Assert.AreEqual(eumUnit.eumUmeter, axis.AxisUnit) + else: + Assert.AreEqual(eumUnit.eumUUnitUndefined, axis.AxisUnit) Assert.AreEqual(2057, axis.XCount) Assert.AreEqual(0, axis.X0) Assert.AreEqual(1, axis.Dx) @@ -703,7 +717,10 @@ def StaticItemTester(dfsFile): axis = staticItem.SpatialAxis Assert.IsNotNull(axis) Assert.AreEqual(1, axis.Dimension) - # TODO: Assert.AreEqual(eumUnit.eumUmeter, axis.AxisUnit); + if original: + Assert.AreEqual(eumUnit.eumUmeter, axis.AxisUnit) + else: + Assert.AreEqual(eumUnit.eumUUnitUndefined, axis.AxisUnit) Assert.AreEqual(3 * 3636, axis.XCount) Assert.AreEqual(0, axis.X0) Assert.AreEqual(1, axis.Dx) From 3b86e3eb6e2b831c71cf6e89de9d3ae81e32c290 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:05:27 +0200 Subject: [PATCH 4/7] Read into the given itemData in the Odense ReadTester The loop carried over from C# had the reuse call commented out with "TODO: Implement?" and allocated a new itemData each time instead. ReadItemTimeStep(itemData, j) exists, so the loop now calls it, as the other ReadTester loops in test_dfsu2D.py and test_dfsu_file.py do. The values after the loop still match the C# test's. Co-Authored-By: Claude Opus 5.5 --- tests/test_dfsu2D.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_dfsu2D.py b/tests/test_dfsu2D.py index 5f80270..27288f5 100644 --- a/tests/test_dfsu2D.py +++ b/tests/test_dfsu2D.py @@ -1063,10 +1063,7 @@ def ReadTester(dfsFile): for j in range(13): for i in range(3): itemData = itemDatas[i] - # TODO: Implement? - # dfsFile.ReadItemTimeStep(itemData, j); - itemData = dfsFile.ReadItemTimeStep(i + 1, j) - itemDatas[i] = itemData + dfsFile.ReadItemTimeStep(itemData, j) Assert.AreEqual(i + 1, itemData.ItemNumber) Assert.AreEqual(j * 86400, itemData.Time) From 6e4252bcc514ac82caaef9ee8d307baa02585c10 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 13 Oct 2026 16:05:33 +0200 Subject: [PATCH 5/7] Test ReadItemTimeStep(itemData, j) against the allocating overload The ReadTester loops only check ItemNumber and Time, and data after the last timestep. This test reads every item-timestep backwards into one buffer per item, on dfs0, dfs2, dfs3 (also reshaped, through Dfs123File) and dfsu, and checks that the call returns the given itemData, keeps its buffer, sets TimeStepIndex and matches ReadItemTimeStep(itemNumber, j). Co-Authored-By: Claude Opus 5.5 --- tests/test_dfs_basic.py | 40 ++++++++++++++++++++++++++++++++++++++++ tests/test_dfsu2D.py | 2 ++ 2 files changed, 42 insertions(+) diff --git a/tests/test_dfs_basic.py b/tests/test_dfs_basic.py index fb70f39..27ee9c4 100644 --- a/tests/test_dfs_basic.py +++ b/tests/test_dfs_basic.py @@ -1,5 +1,6 @@ import ctypes +import numpy as np import pytest from mikecore.DfsDLL import DfsDLL @@ -51,6 +52,45 @@ def test_read_itemtimestep(): assert data.Data[0] == 104 +@pytest.mark.parametrize( + "opener, filename, reshape", + [ + ("DfsGenericOpen", "testdata/TemporalNeqCal.dfs0", False), + ("Dfs2FileOpen", "testdata/OresundHD.dfs2", False), + ("Dfs2FileOpen", "testdata/OresundHD.dfs2", True), + ("Dfs3FileOpen", "testdata/OresundHD.dfs3", True), + ("DfsGenericOpen", "testdata/OdenseHD2D.dfsu", False), + ], + ids=["dfs0", "dfs2", "dfs2-reshape", "dfs3-reshape", "dfsu"], +) +def test_read_itemtimestep_into_given_itemdata(opener, filename, reshape): + """ReadItemTimeStep(itemData, j) fills and returns the itemData it is given, + with the same values as ReadItemTimeStep(itemNumber, j) allocates.""" + dfs = getattr(DfsFileFactory, opener)(filename) + numberOfTimeSteps = dfs.FileInfo.TimeAxis.NumberOfTimeSteps + assert numberOfTimeSteps > 1 + + changed = False + for itemNumber in range(1, len(dfs.ItemInfo) + 1): + itemData = dfs.ReadItemTimeStep(itemNumber, numberOfTimeSteps - 1, reshape) + buffer = itemData.Data + # Backwards, so the buffer holds another timestep's values before each read + for j in reversed(range(numberOfTimeSteps)): + expected = dfs.ReadItemTimeStep(itemNumber, j, reshape) + before = itemData.Data.copy() + + assert dfs.ReadItemTimeStep(itemData, j, reshape) is itemData + assert np.shares_memory(itemData.Data, buffer) + assert itemNumber == itemData.ItemNumber + assert j == itemData.TimeStepIndex + assert expected.Time == itemData.Time + np.testing.assert_array_equal(expected.Data, itemData.Data) + changed = changed or not np.array_equal(before, itemData.Data) + dfs.Close() + + assert changed, "every read left the buffer as it was" + + def test_error_reporting(): print(DfsDLL.dfsErrorString(1000)) diff --git a/tests/test_dfsu2D.py b/tests/test_dfsu2D.py index 27288f5..ad12116 100644 --- a/tests/test_dfsu2D.py +++ b/tests/test_dfsu2D.py @@ -1060,6 +1060,8 @@ def ReadTester(dfsFile): for itemInfo in dfsFile.ItemInfo: itemDatas.append(dfsFile.CreateEmptyItemData(itemInfo)) + # test_dfs_basic.py::test_read_itemtimestep_into_given_itemdata checks this + # overload against ReadItemTimeStep(itemNumber, j) for every item-timestep. for j in range(13): for i in range(3): itemData = itemDatas[i] From e9a97e058e67fe2e42e1f3c00de57a06c1b48793 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 20 Oct 2026 09:02:06 +0200 Subject: [PATCH 6/7] Read into a flat itemData so the reshape cases reshape it The buffer came from the reshaping ReadItemTimeStep, so it was already 2D or 3D and the Dfs123File reshape never ran on it. Start from CreateEmptyItemData and assert the shape after each read; removing the reshape now fails the reshape cases. Drop the ItemNumber assertion, which only checked the test's own input, and pass the openers themselves instead of their names. Co-Authored-By: Claude Opus 5.5 --- tests/test_dfs_basic.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_dfs_basic.py b/tests/test_dfs_basic.py index 27ee9c4..9026d80 100644 --- a/tests/test_dfs_basic.py +++ b/tests/test_dfs_basic.py @@ -53,39 +53,40 @@ def test_read_itemtimestep(): @pytest.mark.parametrize( - "opener, filename, reshape", + "opener, filename, reshape, shape", [ - ("DfsGenericOpen", "testdata/TemporalNeqCal.dfs0", False), - ("Dfs2FileOpen", "testdata/OresundHD.dfs2", False), - ("Dfs2FileOpen", "testdata/OresundHD.dfs2", True), - ("Dfs3FileOpen", "testdata/OresundHD.dfs3", True), - ("DfsGenericOpen", "testdata/OdenseHD2D.dfsu", False), + (DfsFileFactory.DfsGenericOpen, "testdata/TemporalNeqCal.dfs0", False, (1,)), + (DfsFileFactory.Dfs2FileOpen, "testdata/OresundHD.dfs2", False, (6461,)), + (DfsFileFactory.Dfs2FileOpen, "testdata/OresundHD.dfs2", True, (71, 91)), + (DfsFileFactory.Dfs3FileOpen, "testdata/OresundHD.dfs3", True, (72, 94, 41)), + (DfsFileFactory.DfsGenericOpen, "testdata/OdenseHD2D.dfsu", False, (724,)), ], ids=["dfs0", "dfs2", "dfs2-reshape", "dfs3-reshape", "dfsu"], ) -def test_read_itemtimestep_into_given_itemdata(opener, filename, reshape): +def test_read_itemtimestep_into_given_itemdata(opener, filename, reshape, shape): """ReadItemTimeStep(itemData, j) fills and returns the itemData it is given, with the same values as ReadItemTimeStep(itemNumber, j) allocates.""" - dfs = getattr(DfsFileFactory, opener)(filename) + dfs = opener(filename) numberOfTimeSteps = dfs.FileInfo.TimeAxis.NumberOfTimeSteps assert numberOfTimeSteps > 1 changed = False for itemNumber in range(1, len(dfs.ItemInfo) + 1): - itemData = dfs.ReadItemTimeStep(itemNumber, numberOfTimeSteps - 1, reshape) + # Flat, so the reshape cases reshape it on read + itemData = dfs.CreateEmptyItemData(itemNumber) buffer = itemData.Data # Backwards, so the buffer holds another timestep's values before each read for j in reversed(range(numberOfTimeSteps)): expected = dfs.ReadItemTimeStep(itemNumber, j, reshape) - before = itemData.Data.copy() + before = buffer.copy() assert dfs.ReadItemTimeStep(itemData, j, reshape) is itemData assert np.shares_memory(itemData.Data, buffer) - assert itemNumber == itemData.ItemNumber + assert shape == itemData.Data.shape assert j == itemData.TimeStepIndex assert expected.Time == itemData.Time np.testing.assert_array_equal(expected.Data, itemData.Data) - changed = changed or not np.array_equal(before, itemData.Data) + changed = changed or not np.array_equal(before, buffer) dfs.Close() assert changed, "every read left the buffer as it was" From 6a0c56a37e55853dab4433d0a3ec269f92684185 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Tue, 20 Oct 2026 09:02:10 +0200 Subject: [PATCH 7/7] Make the Oresund StaticItemTester original flag required Every caller now says which kind of file it passes, as the testers in test_dfsu_file.py do. Drop the Odense ItemNumber assertion, which only checked the itemData the loop created, and reword the comment so it names no other test. Co-Authored-By: Claude Opus 5.5 --- tests/test_dfsu2D.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_dfsu2D.py b/tests/test_dfsu2D.py index ad12116..59a89c9 100644 --- a/tests/test_dfsu2D.py +++ b/tests/test_dfsu2D.py @@ -68,7 +68,7 @@ def test_CreateOresundHDTest(self): FileOresundHDDfsu.FileInfoTester(dfsFile) FileOresundHDDfsu.CustomBlockTester(dfsFile) FileOresundHDDfsu.DynamicItemTester(dfsFile) - FileOresundHDDfsu.StaticItemTester(dfsFile) + FileOresundHDDfsu.StaticItemTester(dfsFile, original=False) FileOresundHDDfsu.ReadTester(dfsFile) def test_ReadOresundHDTest(self): @@ -307,7 +307,7 @@ def test_CreateOresundHDGenericTest(self): FileOresundHDDfsu.FileInfoTester(dfsFile) FileOresundHDDfsu.CustomBlockTester(dfsFile) FileOresundHDDfsu.DynamicItemTester(dfsFile) - FileOresundHDDfsu.StaticItemTester(dfsFile) + FileOresundHDDfsu.StaticItemTester(dfsFile, original=False) FileOresundHDDfsu.ReadTester(dfsFile) def test_ExtractDfsu2DTopLayerFrom3DIsThe2DModel(self): @@ -589,7 +589,7 @@ def DynamicItemTester(dfsFile): Assert.AreEqual(1, axis.Dx) @staticmethod - def StaticItemTester(dfsFile, original=False): + def StaticItemTester(dfsFile, original): staticItems = [] staticItemNumber = 1 @@ -1060,13 +1060,12 @@ def ReadTester(dfsFile): for itemInfo in dfsFile.ItemInfo: itemDatas.append(dfsFile.CreateEmptyItemData(itemInfo)) - # test_dfs_basic.py::test_read_itemtimestep_into_given_itemdata checks this - # overload against ReadItemTimeStep(itemNumber, j) for every item-timestep. + # This overload is checked against ReadItemTimeStep(itemNumber, j) for + # every item-timestep in a separate test, so this loop checks only Time. for j in range(13): for i in range(3): itemData = itemDatas[i] dfsFile.ReadItemTimeStep(itemData, j) - Assert.AreEqual(i + 1, itemData.ItemNumber) Assert.AreEqual(j * 86400, itemData.Time) Assert.AreEqual(np.float32(0.22424224), itemDatas[0].Data[0])