diff --git a/mikeio/dataset/_dataarray.py b/mikeio/dataset/_dataarray.py index 9d669126c..389c17f9f 100644 --- a/mikeio/dataset/_dataarray.py +++ b/mikeio/dataset/_dataarray.py @@ -254,8 +254,6 @@ def _parse_geometry( if dims == ("time", "x"): return Grid1D(nx=shape[1], dx=1.0 / (shape[1] - 1)) - warnings.warn("Geometry is required for ndim >=1") - axis = 1 if "time" in dims else 0 # dims_no_time = tuple([d for d in dims if d != "time"]) # shape_no_time = shape[1:] if ("time" in dims) else shape @@ -292,6 +290,9 @@ def _parse_geometry( assert shape[axis + 1] == geometry.nx, "data shape does not match nx" # elif isinstance(geometry, Grid3D): # TODO + if geometry is None: + geometry = GeometryUndefined() + return geometry @staticmethod @@ -1669,8 +1670,16 @@ def _apply_math_operation( # TODO: check if geometry etc match if other is DataArray? - new_da = self.copy() # TODO: alternatively: create new dataset (will validate) - new_da.values = data + # new_da = self.copy() # TODO: alternatively: create new dataset (will validate) + # new_da.values = data + + time = self.time + if isinstance(other, DataArray): + time = other.time if len(self.time) == 1 else self.time + + new_da = DataArray( + data=data, time=time, geometry=self.geometry, item=self.item, zn=self._zn + ) if not self._keep_EUM_after_math_operation(other, func): other_name = other.name if hasattr(other, "name") else "array" diff --git a/mikeio/dataset/_dataset.py b/mikeio/dataset/_dataset.py index 77cc84b42..7850d5634 100644 --- a/mikeio/dataset/_dataset.py +++ b/mikeio/dataset/_dataset.py @@ -1696,17 +1696,12 @@ def __mul__(self, other: "Dataset" | float) -> "Dataset": def _add_dataset(self, other: "Dataset", sign: float = 1.0) -> "Dataset": self._check_datasets_match(other) - try: - data = [ - self[x].to_numpy() + sign * other[y].to_numpy() - for x, y in zip(self.items, other.items) - ] - except TypeError: - raise TypeError("Could not add data in Dataset") - newds = self.copy() - for j in range(len(self)): - newds[j].values = data[j] # type: ignore - return newds + das = [] + for da1, da2 in zip(self, other): + da = da1 + sign * da2 + das.append(da) + + return Dataset(das, validate=False) def _check_datasets_match(self, other: "Dataset") -> None: if self.n_items != other.n_items: @@ -1722,10 +1717,9 @@ def _check_datasets_match(self, other: "Dataset") -> None: raise ValueError( f"Item units must match. Item {j}: {self.items[j].unit} != {other.items[j].unit}" ) - if not np.all(self.time == other.time): - raise ValueError("All timesteps must match") - if self.shape != other.shape: - raise ValueError("shape must match") + if len(self.time) > 1 and len(other.time) > 1: + if not np.all(self.time == other.time): + raise ValueError("All timesteps must match") def _add_value(self, value: float) -> "Dataset": try: diff --git a/tests/test_dataarray.py b/tests/test_dataarray.py index 3d2815e5e..2d0c83255 100644 --- a/tests/test_dataarray.py +++ b/tests/test_dataarray.py @@ -167,31 +167,6 @@ def test_data_0d(da0): assert "values" in repr(da0) -def test_create_data_1d_default_grid(): - da = mikeio.DataArray( - data=np.zeros((10, 5)), - time=pd.date_range(start="2000-01-01", freq="h", periods=10), - item=ItemInfo("Foo"), - ) - assert isinstance(da.geometry, mikeio.Grid1D) - - -# def test_data_2d_no_geometry_not_allowed(): - -# nt = 10 -# nx = 7 -# ny = 14 - -# with pytest.warns(Warning) as w: -# mikeio.DataArray( -# data=np.zeros([nt, ny, nx]) + 0.1, -# time=pd.date_range(start="2000-01-01", freq="S", periods=nt), -# item=ItemInfo("Foo"), -# ) - -# assert "geometry" in str(w[0].message).lower() - - def test_dataarray_init(): nt = 10 start = 10.0 @@ -239,15 +214,16 @@ def test_dataarray_init_2d(): # 2d with time ny, nx = 5, 6 + geometry = mikeio.Grid2D(ny=ny, nx=nx, dx=1.0) data2d = np.zeros([nt, ny, nx]) + 0.1 - da = mikeio.DataArray(data=data2d, time=time) + da = mikeio.DataArray(data=data2d, time=time, geometry=geometry) assert da.ndim == 3 assert da.dims == ("time", "y", "x") # singleton time, requires spec of dims dims = ("time", "y", "x") data2d = np.zeros([1, ny, nx]) + 0.1 - da = mikeio.DataArray(data=data2d, time="2018", dims=dims) + da = mikeio.DataArray(data=data2d, time="2018", dims=dims, geometry=geometry) assert isinstance(da, mikeio.DataArray) assert da.n_timesteps == 1 assert da.ndim == 3 @@ -255,40 +231,40 @@ def test_dataarray_init_2d(): # no time data2d = np.zeros([ny, nx]) + 0.1 - da = mikeio.DataArray(data=data2d, time="2018") + da = mikeio.DataArray(data=data2d, time="2018", geometry=geometry) assert isinstance(da, mikeio.DataArray) assert da.n_timesteps == 1 assert da.ndim == 2 assert da.dims == ("y", "x") - # x, y swapped - dims = ("x", "y") - data2d = np.zeros([nx, ny]) + 0.1 - da = mikeio.DataArray(data=data2d, time="2018", dims=dims) - assert da.n_timesteps == 1 - assert da.ndim == 2 - assert da.dims == dims + # # x, y swapped + # dims = ("x", "y") + # data2d = np.zeros([nx, ny]) + 0.1 + # da = mikeio.DataArray(data=data2d, time="2018", dims=dims) + # assert da.n_timesteps == 1 + # assert da.ndim == 2 + # assert da.dims == dims -def test_dataarray_init_5d(): - nt = 10 - time = pd.date_range(start="2000-01-01", freq="s", periods=nt) +# def test_dataarray_init_5d(): +# nt = 10 +# time = pd.date_range(start="2000-01-01", freq="S", periods=nt) - # 5d with named dimensions - dims = ("x", "y", "layer", "member", "season") - data5d = np.zeros([2, 4, 5, 3, 3]) + 0.1 - da = mikeio.DataArray(data=data5d, time="2018", dims=dims) - assert da.n_timesteps == 1 - assert da.ndim == 5 - assert da.dims == dims +# # 5d with named dimensions +# dims = ("x", "y", "layer", "member", "season") +# data5d = np.zeros([2, 4, 5, 3, 3]) + 0.1 +# da = mikeio.DataArray(data=data5d, time="2018", dims=dims) +# assert da.n_timesteps == 1 +# assert da.ndim == 5 +# assert da.dims == dims - # 5d with named dimensions and time - dims = ("time", "dummy", "layer", "member", "season") - data5d = np.zeros([nt, 4, 5, 3, 3]) + 0.1 - da = mikeio.DataArray(data=data5d, time=time, dims=dims) - assert da.n_timesteps == nt - assert da.ndim == 5 - assert da.dims == dims +# # 5d with named dimensions and time +# dims = ("time", "dummy", "layer", "member", "season") +# data5d = np.zeros([nt, 4, 5, 3, 3]) + 0.1 +# da = mikeio.DataArray(data=data5d, time=time, dims=dims) +# assert da.n_timesteps == nt +# assert da.ndim == 5 +# assert da.dims == dims def test_dataarray_init_wrong_dim(): @@ -832,8 +808,9 @@ def test_modify_values_1d(da1): assert da1.values[4] == 12.0 # values is scalar, therefore copy by definition. Original is not changed. - # TODO is the treatment of scalar sensible, i.e. consistent with xarray? - da1.isel(4).values = 11.0 + da1.isel(4).values = ( + 11.0 # TODO is the treatment of scalar sensible, i.e. consistent with xarray? + ) assert da1.values[4] != 11.0 # fancy indexing will return copy! Original is *not* changed. @@ -992,6 +969,23 @@ def test_multiply_two_dataarrays_broadcasting(da_grid2d): assert da_grid2d.shape == da3.shape +def test_math_broadcasting(da1): + da2 = da1.mean("time") + + da3 = da1 - da2 + assert isinstance(da3, mikeio.DataArray) + assert da1.shape == da3.shape + + da3 = da1 + da2 + assert isinstance(da3, mikeio.DataArray) + assert da1.shape == da3.shape + + # + is commutative + da4 = da2 + da1 + assert isinstance(da4, mikeio.DataArray) + assert da1.shape == da4.shape + + def test_math_two_dataarrays(da1): da3 = da1 + da1 assert isinstance(da3, mikeio.DataArray) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 327ae990f..2c126f493 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -41,7 +41,6 @@ def ds2(): @pytest.fixture def ds3(): - nt = 100 d1 = np.zeros([nt, 100, 30]) + 1.5 d2 = np.zeros([nt, 100, 30]) + 2.0 @@ -55,7 +54,6 @@ def ds3(): def test_create_wrong_data_type_error(): - data = ["item 1", "item 2"] nt = 2 @@ -66,7 +64,6 @@ def test_create_wrong_data_type_error(): def test_get_names(): - nt = 100 d = np.zeros([nt, 100, 30]) + 1.0 time = pd.date_range(start=datetime(2000, 1, 1), freq="s", periods=nt) @@ -138,7 +135,6 @@ def test_remove(ds1): def test_index_with_attribute(): - nt = 10000 d = np.zeros([nt, 100, 30]) + 1.0 time = pd.date_range(start=datetime(2000, 1, 1), freq="s", periods=nt) @@ -208,7 +204,6 @@ def test_getitem_multi_indexing_attempted(ds3): def test_select_subset_isel(): - nt = 100 d1 = np.zeros([nt, 100, 30]) + 1.5 d2 = np.zeros([nt, 100, 30]) + 2.0 @@ -246,7 +241,6 @@ def test_select_subset_isel(): def test_select_subset_isel_axis_out_of_range_error(ds2): - assert len(ds2.shape) == 2 dss = ds2.isel(idx=0) @@ -263,7 +257,6 @@ def test_isel_named_axis(ds2: mikeio.Dataset): def test_select_temporal_subset_by_idx(): - nt = 100 d1 = np.zeros([nt, 100, 30]) + 1.5 d2 = np.zeros([nt, 100, 30]) + 2.0 @@ -283,7 +276,6 @@ def test_select_temporal_subset_by_idx(): def test_temporal_subset_fancy(): - nt = (24 * 31) + 1 d1 = np.zeros([nt, 100, 30]) + 1.5 d2 = np.zeros([nt, 100, 30]) + 2.0 @@ -432,7 +424,6 @@ def test_select_item_by_iteminfo(): def test_select_subset_isel_multiple_idxs(): - nt = 100 d1 = np.zeros([nt, 100, 30]) + 1.5 d2 = np.zeros([nt, 100, 30]) + 2.0 @@ -458,7 +449,6 @@ def test_decribe(ds1): def test_create_undefined(): - nt = 100 d1 = np.zeros([nt]) d2 = np.zeros([nt]) @@ -480,7 +470,6 @@ def test_create_undefined(): def test_create_named_undefined(): - nt = 100 d1 = np.zeros([nt]) d2 = np.zeros([nt]) @@ -497,7 +486,6 @@ def test_create_named_undefined(): def test_to_dataframe_single_timestep(): - nt = 1 d1 = np.zeros([nt]) d2 = np.zeros([nt]) @@ -517,7 +505,6 @@ def test_to_dataframe_single_timestep(): def test_to_dataframe(): - nt = 100 d1 = np.zeros([nt]) d2 = np.zeros([nt]) @@ -534,7 +521,6 @@ def test_to_dataframe(): def test_to_pandas_single_item_dataset(): - da = mikeio.DataArray( data=np.zeros(5), time=pd.date_range("2000", freq="D", periods=5), item="Foo" ) @@ -547,7 +533,6 @@ def test_to_pandas_single_item_dataset(): def test_multidimensional_to_dataframe_no_supported(): - nt = 100 d1 = np.zeros([nt, 2]) @@ -560,7 +545,6 @@ def test_multidimensional_to_dataframe_no_supported(): def test_get_data(): - data = [] nt = 100 d = np.zeros([nt, 100, 30]) + 1.0 @@ -573,7 +557,6 @@ def test_get_data(): def test_interp_time(): - nt = 4 d = np.zeros([nt, 10, 3]) d[1] = 2.0 @@ -595,7 +578,6 @@ def test_interp_time(): def test_interp_time_to_other_dataset(): - # Arrange ## mikeio.Dataset 1 nt = 4 @@ -678,7 +660,6 @@ def test_extrapolate_not_allowed(): def test_get_data_2(): - nt = 100 data = [] d = np.zeros([nt, 100, 30]) + 1.0 @@ -691,7 +672,6 @@ def test_get_data_2(): def test_get_data_name(): - nt = 100 data = [] d = np.zeros([nt, 100, 30]) + 1.0 @@ -704,7 +684,6 @@ def test_get_data_name(): def test_modify_selected_variable(): - nt = 100 time = pd.date_range("2000-1-2", freq="h", periods=nt) @@ -734,7 +713,6 @@ def test_get_bad_name(): def test_flipud(): - nt = 2 d = np.random.random([nt, 100, 30]) time = pd.date_range("2000-1-2", freq="h", periods=nt) @@ -799,7 +777,6 @@ def test_aggregations(): def test_to_dfs_extension_validation(tmp_path): - outfilename = tmp_path / "not_gonna_happen.dfs2" ds = mikeio.read( @@ -882,7 +859,6 @@ def test_nanquantile(): def test_aggregate_across_items(): - ds = mikeio.read("tests/testdata/State_wlbc_north_err.dfs1") dsm = ds.mean(axis="items") @@ -901,7 +877,6 @@ def test_aggregate_across_items(): def test_aggregate_selected_items_dfsu_save_to_new_file(tmp_path): - ds = mikeio.read("tests/testdata/State_Area.dfsu", items="*Level*") assert ds.n_items == 5 @@ -963,14 +938,12 @@ def test_dropna(): def test_default_type(): - item = ItemInfo("Foo") assert item.type == EUMType.Undefined assert repr(item.unit) == "undefined" def test_int_is_valid_type_info(): - item = ItemInfo("Foo", 100123) assert item.type == EUMType.Viscosity @@ -979,7 +952,6 @@ def test_int_is_valid_type_info(): def test_int_is_valid_unit_info(): - item = ItemInfo("U", 100002, 2000) assert item.type == EUMType.Wind_Velocity assert item.unit == EUMUnit.meter_per_sec @@ -987,7 +959,6 @@ def test_int_is_valid_unit_info(): def test_default_unit_from_type(): - item = ItemInfo("Foo", EUMType.Water_Level) assert item.type == EUMType.Water_Level assert item.unit == EUMUnit.meter @@ -1005,7 +976,6 @@ def test_default_unit_from_type(): def test_default_name_from_type(): - item = ItemInfo(EUMType.Current_Speed) assert item.name == "Current Speed" assert item.unit == EUMUnit.meter_per_sec @@ -1020,14 +990,11 @@ def test_default_name_from_type(): def test_iteminfo_string_type_should_fail_with_helpful_message(): - with pytest.raises(ValueError): - ItemInfo("Water level", "Water level") def test_item_search(): - res = EUMType.search("level") assert len(res) > 0 @@ -1035,7 +1002,6 @@ def test_item_search(): def test_dfsu3d_dataset(): - filename = "tests/testdata/oresund_sigma_z.dfsu" dfsu = mikeio.open(filename) @@ -1064,7 +1030,6 @@ def test_dfsu3d_dataset(): def test_items_data_mismatch(): - nt = 100 d = np.zeros([nt, 100, 30]) + 1.0 time = pd.date_range("2000-1-2", freq="h", periods=nt) @@ -1075,7 +1040,6 @@ def test_items_data_mismatch(): def test_time_data_mismatch(): - nt = 100 d = np.zeros([nt, 100, 30]) + 1.0 time = pd.date_range( @@ -1136,7 +1100,6 @@ def test_create_empty_data(): def test_create_infer_name_from_eum(): - nt = 100 d = np.random.uniform(size=nt) @@ -1160,8 +1123,25 @@ def test_add_scalar(ds1): assert np.all(ds3[1].to_numpy() == ds2[1].to_numpy()) -def test_add_inconsistent_dataset(ds1): +def test_subtract_two_datasets(ds1): + ds2 = ds1.mean("time") + + ds3 = ds1 - ds2 + assert ds3.shape == ds1.shape + +def test_add_two_datasets(ds1): + ds2 = ds1.mean("time") + + ds3 = ds1 + ds2 + assert ds3.shape == ds1.shape + + # + is commutative + ds4 = ds2 + ds1 + assert ds4.shape == ds1.shape + + +def test_add_inconsistent_dataset(ds1): ds2 = ds1[[0]] assert len(ds1) != len(ds2) @@ -1174,13 +1154,11 @@ def test_add_inconsistent_dataset(ds1): def test_add_bad_value(ds1): - with pytest.raises(TypeError): ds1 + ["one"] def test_multiple_bad_value(ds1): - with pytest.raises(TypeError): ds1 * ["pi"] @@ -1384,7 +1362,6 @@ def test_merge_by_item_dfsu_3d(): def test_to_numpy(ds2): - X = ds2.to_numpy() assert X.shape == (ds2.n_items,) + ds2.shape @@ -1460,7 +1437,6 @@ def test_merge_same_name_error(): def test_incompatible_data_not_allowed(): - da1 = mikeio.read("tests/testdata/HD2D.dfsu")[0] da2 = mikeio.read("tests/testdata/oresundHD_run1.dfsu")[1] @@ -1543,7 +1519,6 @@ def test_create_dataset_with_many_items(): def test_create_array_with_defaults_from_dataset(): - filename = "tests/testdata/oresund_sigma_z.dfsu" ds: mikeio.Dataset = mikeio.read(filename)