From cbf5bfd18b1c851c403eb9970166e135fcff4634 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:55:56 +0100 Subject: [PATCH 1/4] First attempt --- linopy/expressions.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/linopy/expressions.py b/linopy/expressions.py index 10e243de..ac27c468 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -2122,6 +2122,41 @@ def merge( data = [e.data if isinstance(e, linopy_types) else e for e in exprs] data = [fill_missing_coords(ds, fill_helper_dims=True) for ds in data] + # When using join='override', xr.concat places values positionally instead of + # aligning by label. We need to reindex datasets that have the same coordinate + # values but in a different order to ensure proper alignment. + if override and len(data) > 1: + reference = data[0] + aligned_data = [reference] + for ds in data[1:]: + needs_reindex = False + for dim in reference.dims: + if dim in HELPER_DIMS or dim not in ds.dims: + continue + if dim not in reference.coords or dim not in ds.coords: + continue + ref_coord = reference.coords[dim].values + ds_coord = ds.coords[dim].values + # Check: same length, same set of values, but different order + if len(ref_coord) == len(ds_coord) and not np.array_equal( + ref_coord, ds_coord + ): + try: + same_values = set(ref_coord) == set(ds_coord) + except TypeError: + # Unhashable types (e.g., tuples) - convert to strings + same_values = {str(v) for v in ref_coord} == { + str(v) for v in ds_coord + } + if same_values: + needs_reindex = True + break + if needs_reindex: + aligned_data.append(ds.reindex_like(reference)) + else: + aligned_data.append(ds) + data = aligned_data + if not kwargs: kwargs = { "coords": "minimal", From 09ecc7ccaf5a5dc85f5eb2a13ebd611272a87f85 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 20 Jan 2026 19:59:20 +0100 Subject: [PATCH 2/4] Second attempt --- linopy/expressions.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/linopy/expressions.py b/linopy/expressions.py index ac27c468..1461200a 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -2129,7 +2129,7 @@ def merge( reference = data[0] aligned_data = [reference] for ds in data[1:]: - needs_reindex = False + reindex_dims = {} for dim in reference.dims: if dim in HELPER_DIMS or dim not in ds.dims: continue @@ -2149,10 +2149,9 @@ def merge( str(v) for v in ds_coord } if same_values: - needs_reindex = True - break - if needs_reindex: - aligned_data.append(ds.reindex_like(reference)) + reindex_dims[dim] = reference.coords[dim] + if reindex_dims: + aligned_data.append(ds.reindex(reindex_dims)) else: aligned_data.append(ds) data = aligned_data From 2902df1e2d3c79790a6e56458c319776fcb0f18b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:12:31 +0100 Subject: [PATCH 3/4] Third attempt --- linopy/expressions.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/linopy/expressions.py b/linopy/expressions.py index 1461200a..51224e40 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -2128,15 +2128,15 @@ def merge( if override and len(data) > 1: reference = data[0] aligned_data = [reference] - for ds in data[1:]: + for ds_item in data[1:]: reindex_dims = {} - for dim in reference.dims: - if dim in HELPER_DIMS or dim not in ds.dims: + for dim_name in reference.dims: + if dim_name in HELPER_DIMS or dim_name not in ds_item.dims: continue - if dim not in reference.coords or dim not in ds.coords: + if dim_name not in reference.coords or dim_name not in ds_item.coords: continue - ref_coord = reference.coords[dim].values - ds_coord = ds.coords[dim].values + ref_coord = reference.coords[dim_name].values + ds_coord = ds_item.coords[dim_name].values # Check: same length, same set of values, but different order if len(ref_coord) == len(ds_coord) and not np.array_equal( ref_coord, ds_coord @@ -2149,11 +2149,11 @@ def merge( str(v) for v in ds_coord } if same_values: - reindex_dims[dim] = reference.coords[dim] + reindex_dims[dim_name] = reference.coords[dim_name] if reindex_dims: - aligned_data.append(ds.reindex(reindex_dims)) + aligned_data.append(ds_item.reindex(reindex_dims)) else: - aligned_data.append(ds) + aligned_data.append(ds_item) data = aligned_data if not kwargs: From eb0bdb714747a063fc8ac91dceeea3742a62e38f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:26:00 +0100 Subject: [PATCH 4/4] - Add test for merge with reordered coordinates to cover the reindexing logic - Mark defensive code paths with pragma: no cover (unreachable in practice) --- linopy/expressions.py | 6 +++--- test/test_linear_expression.py | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/linopy/expressions.py b/linopy/expressions.py index 51224e40..a5b995c2 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -2134,7 +2134,7 @@ def merge( if dim_name in HELPER_DIMS or dim_name not in ds_item.dims: continue if dim_name not in reference.coords or dim_name not in ds_item.coords: - continue + continue # pragma: no cover ref_coord = reference.coords[dim_name].values ds_coord = ds_item.coords[dim_name].values # Check: same length, same set of values, but different order @@ -2143,8 +2143,8 @@ def merge( ): try: same_values = set(ref_coord) == set(ds_coord) - except TypeError: - # Unhashable types (e.g., tuples) - convert to strings + except TypeError: # pragma: no cover + # Unhashable types - convert to strings for comparison same_values = {str(v) for v in ref_coord} == { str(v) for v in ds_coord } diff --git a/test/test_linear_expression.py b/test/test_linear_expression.py index a75ace3f..f2226f81 100644 --- a/test/test_linear_expression.py +++ b/test/test_linear_expression.py @@ -1194,6 +1194,33 @@ def test_merge(x: Variable, y: Variable, z: Variable) -> None: merge(expr1, expr2) +def test_merge_with_override_and_reordered_coords(m: Model) -> None: + """Test merge with join='override' when coordinates have same values but different order.""" + import pandas as pd + + # Create variables with same coordinate values but different order + coords_a = pd.Index(["x", "y", "z"], name="dim_0") + coords_b = pd.Index(["z", "x", "y"], name="dim_0") # Same values, different order + + v1 = m.add_variables(coords=[coords_a], name="v1") + v2 = m.add_variables(coords=[coords_b], name="v2") + + expr1 = 1 * v1 + expr2 = 2 * v2 + + # Merging along _term (default) triggers the override logic because + # both expressions have the same dimension sizes + res = merge([expr1, expr2], cls=LinearExpression) + + # Verify that the coordinates match the first expression's order + assert list(res.coords["dim_0"].values) == ["x", "y", "z"] + # The result should have 2 terms (one from each expression) + assert res.nterm == 2 + # Verify the coefficients are correctly aligned (not mismatched due to positional concat) + assert res.sel(dim_0="x").coeffs.values.tolist() == [1.0, 2.0] + assert res.sel(dim_0="z").coeffs.values.tolist() == [1.0, 2.0] + + def test_linear_expression_outer_sum(x: Variable, y: Variable) -> None: expr = x + y expr2: LinearExpression = sum([x, y]) # type: ignore